diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ca6156..0db6c3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ include(Sanitizers) include(CodeCoverage) include(CompilerCache) include(PrecompiledHeaders) +include(CPM) # C++ standard set(CMAKE_CXX_STANDARD 20) @@ -56,6 +57,9 @@ target_include_directories(graph3 INTERFACE target_compile_features(graph3 INTERFACE cxx_std_20) +# Link tl::expected for optional cycle detection in topological sort +target_link_libraries(graph3 INTERFACE tl::expected) + # Apply compiler warnings set_project_warnings(graph3) diff --git a/agents/edge_list_goal.md b/agents/edge_list_goal.md index 4d9b04d..53dab3f 100644 --- a/agents/edge_list_goal.md +++ b/agents/edge_list_goal.md @@ -2,36 +2,39 @@ The abstract edge list, a complement to the abstract adjacency list, needs to be implemented. -An edge list is a container or range of edge values that can be projected to the `edge_info` -data structure defined in `graph_info.hpp`. This provides an alternative graph representation -focused on edges rather than vertices. - -There are multiple (and subtle forms) of edge list, as follows: -- "edge list" with a space between the words refers to the abstract edge list data structure. -- The "edge_list" namespace (with an underscore) is the namespace for the abstract edge list - implementation. -- "edgelist" (one word) refers to the edgelist view used by the abstract adjacency list data structure. +An edge list is a range of edge values that have a target_id, source_id and optional edge_value. +This can be represented by projecting values on into an `edge_info` data structure defined in +`graph_info.hpp`. This is an example, and other methods should be considered to implement it. + +## Edge Lists Types and Naming +The term edge list has different forms. +- The abstract edge list is a range of values with a `target_id`, `source_id` and optional + `edge_value` members. They can have different names, so the library needs to be able to + enable the user to expose them into those names using the matching CPO functions. `edge_list` + (with an underscore separator) will be used to refer to this. +- `edgelist` (one word) refers to an edge list view of all edges in an adjacency list. This has + not been implemented yet. ## Goals Create an implementation for an edge list in the `graph::edge_list` namespace, a peer to `graph::adj_list`. -In the future, there will be an edgelist view that iterates through all outgoing edges of -all vertices that will also project `edge_info` values. This will allow algorithms to work -equally well against either: -- The abstract edge list data structure, or -- All edges in an abstract adjacency list (via the edgelist view) +Future algorithms that require an edge list must be able to work with either an `edge_list` +or an `edgelist` view without caring which one it is using. -## Important Details +CPOs for `source_id(g,uv)`, `target_id(g,uv)` and `edge_value(g,uv)` need to work on an adjacency list +edge, a `edge_list` value, and an `edgelist` view. This will help unify the overall design. Observations +to consider for implementation: +- Extend the `source_id(g,uv)`, `target_id(g,uv)` and `edge_value(g,uv)` CPOs to handle the `edge_list` values. +- Extend the edge descriptor to handle the `edge_list` values. + +## Reference Implementation `/mnt/d/dev_graph/graph-v2` contains an earlier version of this library (graph-v2) that uses a reference-based define instead of the value-based descriptors. It has a similar directory -layout, including the `edge_list/` directory for the edge list implementation that can be used -as a reference. I don't think our edge list implementation needs descriptors, so the reference -implementation may be able to be copied with little change. - -The adjacency list views for an edge list will be named `edgelist` in order to distinguish from the -abstract edge list data structure in the `edge_list` namespace. +layout, including the `edge_list/` subdirectory for the edge list implementation that can be used +as a reference design. The implementation for this library may need to be different. +## Important Details `graph_info.hpp` contains common definitions of classes/structs that are shared between both the adjacency list and edge list data structures. While `edge_info` is the primary shared type, others could be added in the future. diff --git a/agents/edge_list_plan.md b/agents/edge_list_plan.md new file mode 100644 index 0000000..45a388e --- /dev/null +++ b/agents/edge_list_plan.md @@ -0,0 +1,815 @@ +# Edge List Unification Implementation Plan + +This document provides a detailed, step-by-step implementation plan for the edge list unification +work described in [edge_list_strategy.md](edge_list_strategy.md). Each step is designed to be +executed by an agent and includes specific tasks, files to modify, and tests to create. + +**Branch**: `feature/edge-list-unification` + +**Reference Documents**: +- [edge_list_goal.md](edge_list_goal.md) - Goals and requirements +- [edge_list_strategy.md](edge_list_strategy.md) - Technical strategy and design decisions + +--- + +## Progress Tracking + +| Step | Description | Status | Commit | +|------|-------------|--------|--------| +| 1.1 | Add `is_edge_list_descriptor_v` trait | ✅ Complete | 4997cb3 | +| 1.2 | Add `_has_edge_info_member` concept to `source_id` CPO | ✅ Complete | | +| 1.3 | Add `_is_tuple_like_edge` concept to `source_id` CPO | ✅ Complete | | +| 1.3a | Add `_has_edge_info_member` and `_is_tuple_like_edge` to `target_id` CPO | ✅ Complete | | +| 1.3b | Add `_has_edge_info_member` and `_is_tuple_like_edge` to `edge_value` CPO | ✅ Complete | | +| 1.4 | Extend `source_id` CPO with tiers 5-7 | ✅ Complete | | +| 1.5 | Create tests for `source_id` CPO extensions | ✅ Complete | | +| 2.1 | Extend `target_id` CPO with tiers 5-7 | ✅ Complete | | +| 2.2 | Create tests for `target_id` CPO extensions | ✅ Complete | | +| 3.1 | Extend `edge_value` CPO with tiers 5-7 | ✅ Complete | | +| 3.2 | Create tests for `edge_value` CPO extensions | ✅ Complete | | +| 4.1 | Create `edge_list::edge_descriptor` type | ✅ Complete | 187d8b7 | +| 4.2 | Add `is_edge_list_descriptor` specialization | ✅ Complete | 187d8b7 | +| 4.3 | Create tests for `edge_list::edge_descriptor` | ✅ Complete | 187d8b7 | +| 5.1 | Update `edge_list.hpp` concepts to use unified CPOs | ✅ Complete | 2026-01-31 | +| 5.2 | Create tests for updated edge_list concepts | ✅ Complete | 2026-01-31 | +| 6.1 | Update `graph.hpp` imports | ✅ Complete | 2026-01-31 | +| 6.2 | Final integration tests | ✅ Complete | 2026-01-31 | + +**Legend**: ⬜ Not Started | 🔄 In Progress | ✅ Complete | ❌ Blocked + +**Summary**: All phases complete! Edge list unification fully implemented and tested. +- Phases 1-3: CPO extensions with tiers 5-7 (23 test cases) +- Phase 4: Reference-based edge_descriptor (60 test assertions) +- Phase 5: Updated concepts and type aliases (41 test assertions) +- Phase 6: Integration and final tests (32 test assertions) +- **Total: 156 assertions across 71 test cases, all passing ✅** + +--- + +## Phase 1: Extend `source_id` CPO + +### Step 1.1: Add `is_edge_list_descriptor_v` Trait + +**Goal**: Create the type trait that identifies `edge_list::edge_descriptor` types. + +**Files to create/modify**: +- `include/graph/edge_list/edge_list_traits.hpp` (NEW) + +**Tasks**: +1. Create new file `include/graph/edge_list/edge_list_traits.hpp` +2. Define forward declaration of `edge_list::edge_descriptor` template +3. Define `is_edge_list_descriptor` trait (defaults to `false_type`) +4. Define `is_edge_list_descriptor_v` variable template +5. Add include guard and namespace structure + +**Code to implement**: +```cpp +#pragma once + +#include + +namespace graph::edge_list { + +// Forward declaration - actual type defined in edge_list_descriptor.hpp +template +struct edge_descriptor; + +// Type trait to identify edge_list descriptors +template +struct is_edge_list_descriptor : std::false_type {}; + +template +struct is_edge_list_descriptor> : std::true_type {}; + +template +inline constexpr bool is_edge_list_descriptor_v = is_edge_list_descriptor::value; + +} // namespace graph::edge_list +``` + +**Tests**: None yet (trait needs the actual type to test against, added in Step 4.2) + +**Commit message**: `Add is_edge_list_descriptor trait for edge_list type detection` + +--- + +### Step 1.2: Add `_has_edge_info_member` Concept + +**Goal**: Add concept to detect edge_info-style data member access (Tier 6). + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Locate `namespace _source_id` in `graph_cpo.hpp` +2. Add include for `edge_list_traits.hpp` at top of file +3. Add `_has_edge_info_member` concept after existing concepts + +**Code to add** (inside `namespace _source_id`): +```cpp +// Tier 6: Check for edge_info-style direct data member access +// Must NOT be a descriptor type (to avoid ambiguity with method calls) +template +concept _has_edge_info_member = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + uv.source_id; // data member, not method + } && + !requires(const UV& uv) { + uv.source_id(); // exclude if it's callable (i.e., a method) + }; +``` + +**Tests**: Created in Step 1.5 + +**Commit message**: `Add _has_edge_info_member concept to source_id CPO` + +--- + +### Step 1.3: Add `_is_tuple_like_edge` Concept + +**Goal**: Add concept to detect tuple-like edge types (Tier 7). + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Locate `namespace _source_id` in `graph_cpo.hpp` +2. Add `_is_tuple_like_edge` concept after `_has_edge_info_member` + +**Code to add** (inside `namespace _source_id`): +```cpp +// Tier 7: Check for tuple-like edge (pair, tuple) +// Must NOT be any descriptor type or have edge_info members +template +concept _is_tuple_like_edge = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + !_has_edge_info_member && + requires { + std::tuple_size>::value; + } && + requires(const UV& uv) { + { std::get<0>(uv) }; + { std::get<1>(uv) }; + }; +``` + +**Tests**: Created in Step 1.5 + +**Commit message**: `Add _is_tuple_like_edge concept to source_id CPO` + +--- + +### Step 1.3a: Add Concepts to `target_id` CPO + +**Goal**: Add the same concepts to `target_id` for consistency. + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Locate `namespace _target_id` in `graph_cpo.hpp` +2. Add `_has_edge_info_member` concept (checks `uv.target_id` data member) +3. Add `_is_tuple_like_edge` concept (same as source_id) + +**Status**: ✅ Complete (implemented alongside source_id concepts) + +--- + +### Step 1.3b: Add Concepts to `edge_value` CPO + +**Goal**: Add the same concepts to `edge_value` for consistency. + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Locate `namespace _edge_value` in `graph_cpo.hpp` +2. Add `_has_edge_info_member` concept (checks `uv.value` data member) +3. Add `_is_tuple_like_edge` concept (checks for `std::get<2>` since edge value is third element) + +**Status**: ✅ Complete (implemented alongside source_id concepts) + +**Commit message** (for 1.2, 1.3, 1.3a, 1.3b combined): `Add edge_info and tuple-like concepts to all three CPOs` + +--- + +### Step 1.4: Extend `source_id` CPO with Tiers 5-7 + +**Goal**: Update the `_St` enum and `_Choose` function to support new tiers. + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Rename `_descriptor` to `_adj_list_descriptor` in `_St` enum +2. Add new enum values: `_edge_list_descriptor`, `_edge_info_member`, `_tuple_like` +3. Update `_Choose()` function to check new tiers in order +4. Update `_fn::operator()` to handle new strategies +5. Ensure noexcept propagation for new tiers + +**Enum update**: +```cpp +enum class _St { + _none, + _native_edge_member, + _member, + _adl, + _adj_list_descriptor, // RENAMED from _descriptor + _edge_list_descriptor, // NEW + _edge_info_member, // NEW + _tuple_like // NEW +}; +``` + +**`_Choose()` additions** (after existing tier 4 check): +```cpp +} else if constexpr (_has_edge_list_descriptor) { + return {_St::_edge_list_descriptor, + noexcept(std::declval().source_id())}; +} else if constexpr (_has_edge_info_member) { + return {_St::_edge_info_member, + noexcept(std::declval().source_id)}; +} else if constexpr (_is_tuple_like_edge) { + return {_St::_tuple_like, + noexcept(std::get<0>(std::declval()))}; +``` + +**`operator()` additions**: +```cpp +} else if constexpr (_Choice<_G, _UV>._Strategy == _St::_edge_list_descriptor) { + return uv.source_id(); +} else if constexpr (_Choice<_G, _UV>._Strategy == _St::_edge_info_member) { + return uv.source_id; +} else if constexpr (_Choice<_G, _UV>._Strategy == _St::_tuple_like) { + return std::get<0>(uv); +``` + +**Tests**: Created in Step 1.5 + +**Commit message**: `Extend source_id CPO with tiers 5-7 for edge_list support` + +--- + +### Step 1.5: Create Tests for `source_id` CPO Extensions + +**Goal**: Test all new tiers and ambiguity cases for `source_id`. + +**Files to create**: +- `tests/edge_list/test_edge_list_cpo.cpp` (NEW) +- `tests/edge_list/CMakeLists.txt` (NEW or modify parent) + +**Tasks**: +1. Create test directory structure if needed +2. Create test file with Catch2 framework +3. Test each new tier with appropriate edge types +4. Test ambiguity guards + +**Test cases to implement**: +```cpp +// Test Tier 6: edge_info data member +TEST_CASE("source_id with edge_info", "[cpo][source_id]") { + using EI = graph::edge_info; + EI ei{1, 2}; + std::vector el{ei}; + + auto uid = graph::source_id(el, ei); + REQUIRE(uid == 1); +} + +// Test Tier 7: tuple-like +TEST_CASE("source_id with pair", "[cpo][source_id]") { + std::pair edge{3, 4}; + std::vector> el{edge}; + + auto uid = graph::source_id(el, edge); + REQUIRE(uid == 3); +} + +TEST_CASE("source_id with tuple", "[cpo][source_id]") { + std::tuple edge{5, 6, 1.5}; + std::vector> el{edge}; + + auto uid = graph::source_id(el, edge); + REQUIRE(uid == 5); +} + +// Ambiguity tests +TEST_CASE("source_id prefers data member over tuple", "[cpo][source_id][ambiguity]") { + // Type with both source_id member and tuple-like interface + // Should pick data member (Tier 6) + struct EdgeWithBoth { + int source_id; + int target_id; + template + friend auto get(const EdgeWithBoth& e) { + if constexpr (I == 0) return e.source_id; + else return e.target_id; + } + }; + // ... test that data member tier is selected +} +``` + +**CMakeLists.txt update**: +```cmake +add_executable(test_edge_list_cpo test_edge_list_cpo.cpp) +target_link_libraries(test_edge_list_cpo PRIVATE graph3::graph3 Catch2::Catch2WithMain) +add_test(NAME test_edge_list_cpo COMMAND test_edge_list_cpo) +``` + +**Commit message**: `Add tests for source_id CPO tiers 5-7` + +--- + +## Phase 2: Extend `target_id` CPO + +### Step 2.1: Extend `target_id` CPO with Tiers 5-7 + +**Goal**: Mirror the `source_id` changes to `target_id`. + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Locate `namespace _target_id` in `graph_cpo.hpp` +2. Add same concepts as `source_id` but checking `target_id` member +3. Update `_St` enum with new values +4. Update `_Choose()` and `operator()` functions + +**Key differences from `source_id`**: +- `_has_edge_info_member` checks `uv.target_id` instead of `uv.source_id` +- Tuple tier uses `std::get<1>(uv)` instead of `std::get<0>(uv)` + +**Commit message**: `Extend target_id CPO with tiers 5-7 for edge_list support` + +--- + +### Step 2.2: Create Tests for `target_id` CPO Extensions + +**Goal**: Test all new tiers for `target_id`. + +**Files to modify**: +- `tests/edge_list/test_edge_list_cpo.cpp` + +**Tasks**: +1. Add test cases mirroring `source_id` tests but for `target_id` +2. Verify `target_id` returns correct element (second, not first) + +**Commit message**: `Add tests for target_id CPO tiers 5-7` + +--- + +## Phase 3: Extend `edge_value` CPO + +### Step 3.1: Extend `edge_value` CPO with Tiers 5-7 + +**Goal**: Mirror the changes to `edge_value`. + +**Files to modify**: +- `include/graph/adj_list/detail/graph_cpo.hpp` + +**Tasks**: +1. Locate `namespace _edge_value` in `graph_cpo.hpp` +2. Add concepts checking `uv.value` member or `std::get<2>(uv)` +3. Update `_St` enum with new values +4. Update `_Choose()` and `operator()` functions + +**Key differences**: +- `_has_edge_info_member` checks `uv.value` instead of `uv.source_id` +- Tuple tier uses `std::get<2>(uv)` for the edge value +- Not all edge types have values (pair does not) + +**Commit message**: `Extend edge_value CPO with tiers 5-7 for edge_list support` + +--- + +### Step 3.2: Create Tests for `edge_value` CPO Extensions + +**Goal**: Test edge_value with edge_info and tuple types. + +**Files to modify**: +- `tests/edge_list/test_edge_list_cpo.cpp` + +**Tasks**: +1. Add test cases for `edge_value` with `edge_info` +2. Add test cases for `edge_value` with `tuple` +3. Verify types without values (pair, edge_info without EV) don't satisfy edge_value + +**Commit message**: `Add tests for edge_value CPO tiers 5-7` + +--- + +## Phase 4: Create Edge List Descriptor + +### Step 4.1: Create `edge_list::edge_descriptor` Type + +**Goal**: Implement the lightweight reference-based edge descriptor for edge lists. + +**Files to create**: +- `include/graph/edge_list/edge_list_descriptor.hpp` (NEW) + +**Tasks**: +1. Create new file with proper includes (type_traits, concepts, functional, utility) +2. Implement `edge_descriptor` as a class with private members +3. Use references for all data members to avoid copies +4. Handle `EV = void` case with empty value optimization +5. Use `std::reference_wrapper` for non-void edge values +6. Implement `source_id()`, `target_id()`, and `value()` accessor methods +7. Implement custom comparison operators (compare values, not reference addresses) +8. Delete assignment operators (reference semantics) + +**Key Design Requirements**: +- **Zero-copy construction**: All members are references (`const VId&`, `std::reference_wrapper`) +- **Lightweight handle**: Descriptor size is ~3 pointers (24 bytes on 64-bit) +- **Reference semantics**: Descriptor refers to data, does not own it +- **Encapsulation**: Use class with private members + +**Code to implement**: +```cpp +#pragma once + +#include +#include +#include +#include + +namespace graph::edge_list { + +namespace detail { + struct empty_value { + constexpr auto operator<=>(const empty_value&) const noexcept = default; + }; +} + +template +class edge_descriptor { +public: + using vertex_id_type = VId; + using edge_value_type = EV; + + // Constructor without value (for void EV) + constexpr edge_descriptor(const VId& src, const VId& tgt) + requires std::is_void_v + : source_id_(src), target_id_(tgt), value_() {} + + // Constructor with value (for non-void EV) + template + requires (!std::is_void_v) + constexpr edge_descriptor(const VId& src, const VId& tgt, const E& val) + : source_id_(src), target_id_(tgt), value_(std::cref(val)) {} + + // Copy/move allowed, assignment deleted (reference semantics) + edge_descriptor(const edge_descriptor&) = default; + edge_descriptor(edge_descriptor&&) = default; + edge_descriptor& operator=(const edge_descriptor&) = delete; + edge_descriptor& operator=(edge_descriptor&&) = delete; + + [[nodiscard]] constexpr const VId& source_id() const noexcept { + return source_id_; + } + + [[nodiscard]] constexpr const VId& target_id() const noexcept { + return target_id_; + } + + template + requires (!std::is_void_v) + [[nodiscard]] constexpr const E& value() const noexcept { + return value_.get(); + } + + // Custom comparison operators + constexpr bool operator==(const edge_descriptor& other) const noexcept; + constexpr auto operator<=>(const edge_descriptor& other) const noexcept; + +private: + const VId& source_id_; + const VId& target_id_; + [[no_unique_address]] std::conditional_t, + detail::empty_value, std::reference_wrapper> value_; +}; + +// Deduction guides +template +edge_descriptor(VId, VId) -> edge_descriptor; + +template +edge_descriptor(VId, VId, EV) -> edge_descriptor; + +} // namespace graph::edge_list +``` + +**Commit message**: `Add edge_list::edge_descriptor as reference-based lightweight handle` + +--- + +### Step 4.2: Add `is_edge_list_descriptor` Specialization + +**Goal**: Specialize the trait for the actual descriptor type. + +**Files to modify**: +- `include/graph/edge_list/edge_list_traits.hpp` +- `include/graph/edge_list/edge_list_descriptor.hpp` + +**Tasks**: +1. Include `edge_list_traits.hpp` in `edge_list_descriptor.hpp` +2. Ensure specialization matches the actual type definition +3. Add concept for edge_list descriptor if useful + +**Commit message**: `Specialize is_edge_list_descriptor for edge_descriptor type` + +--- + +### Step 4.3: Create Tests for `edge_list::edge_descriptor` + +**Goal**: Test the reference-based edge descriptor type and its integration with CPOs. + +**Files to create/modify**: +- `tests/edge_list/test_edge_list_descriptor.cpp` (NEW) + +**Tasks**: +1. Test construction (with and without value, from lvalues with references) +2. Test accessor methods return references to underlying data +3. Test `is_edge_list_descriptor_v` trait +4. Test CPOs work with edge_list::edge_descriptor (Tier 5) +5. Test that descriptors reflect changes to underlying data +6. Test with non-trivial types (strings) to verify zero-copy behavior +7. Test comparison operators + +**Test cases**: +```cpp +TEST_CASE("edge_list::edge_descriptor construction", "[edge_list][descriptor]") { + using namespace graph::edge_list; + + // Without value - must use lvalues (descriptor stores references) + int src = 1, tgt = 2; + edge_descriptor e1(src, tgt); + REQUIRE(e1.source_id() == 1); + REQUIRE(e1.target_id() == 2); + REQUIRE(&e1.source_id() == &src); // Verify it's a reference + + // With value + double val = 1.5; + edge_descriptor e2(src, tgt, val); + REQUIRE(e2.source_id() == 1); + REQUIRE(e2.target_id() == 2); + REQUIRE(e2.value() == 1.5); + REQUIRE(&e2.value() == &val); // Verify it's a reference +} + +TEST_CASE("edge_list::edge_descriptor references underlying data", "[edge_list][descriptor]") { + using namespace graph::edge_list; + + std::string src = "vertex_a", tgt = "vertex_b", val = "edge_data"; + edge_descriptor e(src, tgt, val); + + // Modify underlying data - should be visible through descriptor + src = "new_source"; + REQUIRE(e.source_id() == "new_source"); + + val = "new_data"; + REQUIRE(e.value() == "new_data"); +} + + // With value + edge_descriptor e2(3, 4, 1.5); + REQUIRE(e2.source_id() == 3); + REQUIRE(e2.target_id() == 4); + REQUIRE(e2.value() == 1.5); +} + +TEST_CASE("is_edge_list_descriptor_v trait", "[edge_list][traits]") { + using namespace graph::edge_list; + + static_assert(is_edge_list_descriptor_v>); + static_assert(is_edge_list_descriptor_v>); + static_assert(!is_edge_list_descriptor_v); + static_assert(!is_edge_list_descriptor_v>); +} + +TEST_CASE("source_id CPO with edge_list::edge_descriptor", "[cpo][edge_list]") { + using namespace graph::edge_list; + + edge_descriptor e(5, 6); + std::vector> el{e}; + + auto uid = graph::source_id(el, e); + REQUIRE(uid == 5); +} +``` + +**Commit message**: `Add tests for edge_list::edge_descriptor` + +--- + +## Phase 5: Update Edge List Concepts + +### Step 5.1: Update `edge_list.hpp` Concepts + +**Goal**: Modify concepts to use unified CPO pattern. + +**Files to modify**: +- `include/graph/edge_list/edge_list.hpp` + +**Tasks**: +1. Update `basic_sourced_edgelist` concept to use `graph::source_id(el, uv)` +2. Update `basic_sourced_index_edgelist` concept similarly +3. Update `has_edge_value` concept to use `graph::edge_value(el, uv)` +4. Update type aliases to work with new concept definitions + +**Updated concept**: +```cpp +template +concept basic_sourced_edgelist = + std::ranges::input_range && + !std::ranges::range> && + requires(EL& el, std::ranges::range_value_t uv) { + { graph::source_id(el, uv) }; + { graph::target_id(el, uv) } -> std::same_as; + }; +``` + +**Commit message**: `Update edge_list concepts to use unified CPOs` + +**Status**: ✅ COMPLETE (2026-01-31) +- Updated concepts in edge_list.hpp to use graph::adj_list::_cpo_instances:: CPOs +- Fixed basic_sourced_index_edgelist to check std::integral on decayed return type +- Updated type aliases to use CPO pattern with declval +- Fixed edge_value_t to use std::remove_cvref_t to return value type not reference +- Fixed vertex_id_t to use std::remove_cvref_t for consistency with edge_value_t +- Added clarifying comments: basic_sourced_edgelist supports ANY vertex ID type (not just integral) +- Fixed edge_value CPO to check tuple size >= 3 before accessing element 2 +- Created comprehensive test file test_edge_list_concepts.cpp with 14 test cases +- All tests passing (41 assertions) + +--- + +### Step 5.2: Create Tests for Updated Edge List Concepts + +**Status**: ✅ COMPLETE (2026-01-31) - Tests already created in Step 5.1 +- test_edge_list_concepts.cpp covers all required test cases +- Includes concept satisfaction tests for pairs, tuples, edge_info, edge_descriptor +- Tests with string vertex IDs to verify non-integral support +- Type alias validation tests +- Runtime behavior tests with CPOs +- No additional test file needed + +--- + +## Phase 6: Integration + +### Step 6.1: Update `graph.hpp` Imports + +**Goal**: Verify concepts work with various edge types. + +**Files to create/modify**: +- `tests/edge_list/test_edge_list_concepts.cpp` (NEW) + +**Tasks**: +1. Test concept satisfaction with pair, tuple, edge_info +2. Test concept satisfaction with edge_list::edge_descriptor +3. Verify adjacency_list does NOT satisfy basic_sourced_edgelist + +**Test cases**: +```cpp +TEST_CASE("basic_sourced_edgelist concept", "[edge_list][concepts]") { + using namespace graph::edge_list; + + static_assert(basic_sourced_edgelist>>); + static_assert(basic_sourced_edgelist>>); + static_assert(basic_sourced_edgelist>>); + static_assert(basic_sourced_edgelist>>); + + // Should NOT satisfy (nested range = adjacency list pattern) + static_assert(!basic_sourced_edgelist>>); +} +``` + +**Commit message**: `Add tests for edge_list concepts with unified CPOs` + +--- + +## Phase 6: Integration + +### Step 6.1: Update `graph.hpp` Imports + +**Goal**: Ensure all new headers are properly included. + +**Files to modify**: +- `include/graph/graph.hpp` + +**Tasks**: +1. Add include for `edge_list/edge_list_traits.hpp` +2. Add include for `edge_list/edge_list_descriptor.hpp` +3. Remove reference to old `edgelist.hpp` if still present +4. Export edge_list types to graph namespace if appropriate + +**Commit message**: `Update graph.hpp to include edge_list headers` + +**Status**: ✅ COMPLETE (2026-01-31) +- Added #include +- Added #include +- Organized edge_list includes together in proper section +- edge_list.hpp was already included (updated in earlier phase) +- All edge_list tests passing (124 assertions across 55 test cases) +- Full project builds without errors + +--- + +### Step 6.2: Final Integration Tests + +**Goal**: End-to-end tests verifying unified CPO behavior. + +**Files to create/modify**: +- `tests/edge_list/test_edge_list_integration.cpp` (NEW) + +**Tasks**: +1. Test algorithm-like function that accepts any edge range +2. Verify it works with different edge sources +3. Test mixing adj_list edges and edge_list in same compilation unit + +**Test case**: +```cpp +// Generic algorithm that works with any edge source +template + requires graph::edge_list::basic_sourced_edgelist +auto count_self_loops(EdgeRange&& edges) { + int count = 0; + for (auto&& uv : edges) { + if (graph::source_id(edges, uv) == graph::target_id(edges, uv)) { + ++count; + } + } + return count; +} + +TEST_CASE("Algorithm works with different edge sources", "[integration]") { + // With pair + std::vector> pairs{{1,2}, {3,3}, {4,4}}; + REQUIRE(count_self_loops(pairs) == 2); + + // With edge_info + using EI = graph::edge_info; + std::vector infos{{1,2}, {5,5}}; + REQUIRE(count_self_loops(infos) == 1); + + // With edge_list::edge_descriptor + using ED = graph::edge_list::edge_descriptor; + std::vector descs{ED{1,1}, ED{2,3}}; + REQUIRE(count_self_loops(descs) == 1); +} +``` + +**Commit message**: `Add integration tests for unified edge_list CPOs` + +**Status**: ✅ COMPLETE (2026-01-31) +- Created test_edge_list_integration.cpp with 16 comprehensive test cases +- Implemented generic algorithms (count_self_loops, sum_edge_values) that work with ANY edge type +- Verified all edge types work: pairs, tuples, edge_info, edge_list::edge_descriptor +- Tested string vertex IDs (non-integral types) +- Verified multiple edge types can coexist in same compilation unit +- All 32 assertions passing + +**Complete Edge List Test Suite**: +- test_edge_list_cpo: 23 assertions (CPO tier 5-7 tests) +- test_edge_list_descriptor: 60 assertions (reference-based descriptor tests) +- test_edge_list_concepts: 41 assertions (concept satisfaction tests) +- test_edge_list_integration: 32 assertions (end-to-end integration tests) +- **Total: 156 assertions across 71 test cases** + +--- + +## Build and Run Commands + +```bash +# Configure +cmake --preset linux-gcc-debug + +# Build tests +cmake --build build/linux-gcc-debug --target test_edge_list_cpo test_edge_list_descriptor test_edge_list_concepts test_edge_list_integration + +# Run tests +ctest --test-dir build/linux-gcc-debug -R edge_list --output-on-failure +``` + +--- + +## Completion Checklist + +- [ ] All steps marked ✅ Complete +- [ ] All tests pass +- [ ] No regressions in existing tests (`ctest --test-dir build/linux-gcc-debug`) +- [ ] Code compiles with both GCC and Clang +- [ ] Update edge_list_strategy.md revision history +- [ ] Create PR to merge `feature/edge-list-unification` into `main` + +--- + +## Revision History + +| Date | Version | Changes | +|------|---------|---------| +| 2026-01-31 | 1.0 | Initial implementation plan | diff --git a/agents/edge_list_strategy.md b/agents/edge_list_strategy.md index 3a64023..eec1ae3 100644 --- a/agents/edge_list_strategy.md +++ b/agents/edge_list_strategy.md @@ -9,13 +9,22 @@ The edge list implementation provides an alternative graph representation focuse than vertices. It complements the existing adjacency list (`graph::adj_list`) implementation by allowing algorithms to work directly with edge ranges. +**The key unification goal**: `source_id(g,uv)`, `target_id(g,uv)`, and `edge_value(g,uv)` CPOs +must work uniformly across: +1. **Adjacency list edges** - edges from `edges(g, u)` where `g` is an adjacency list +2. **Edge list values** - elements in a `graph::edge_list` range +3. **Edgelist views** - edges from an `edgelist(g)` view over an adjacency list + +This unification enables algorithms to work with any edge source without specialization. + ### Key Terminology | Term | Description | |------|-------------| -| `edge list` | The abstract concept of a list of edges | -| `edge_list` | The namespace `graph::edge_list` containing the implementation | -| `edgelist` | Views for iterating over all edges (e.g., `edgelist(g)` view over adjacency list) | +| `edge list` | The abstract concept: a range of edges with source_id, target_id, and optional edge_value | +| `edge_list` | The namespace `graph::edge_list` containing implementations (underscore separator) | +| `edgelist` | A view that iterates over all edges in an adjacency list (one word, no separator) | +| `edgelist(g)` | CPO that creates an edgelist view from an adjacency list graph | --- @@ -23,17 +32,25 @@ allowing algorithms to work directly with edge ranges. ### Existing Components -1. **`include/graph/edgelist.hpp`** - Contains a basic `edgelist` container class: - - `graph::edge` - Simple edge struct with source/target/value - - `graph::edgelist` - Vector-based edge container - - Basic iterator support and modifiers +1. **`include/graph/edge_list/edge_list.hpp`** - Edge list concepts and types: + - `graph::edge_list::basic_sourced_edgelist` - Core edgelist concept + - `graph::edge_list::basic_sourced_index_edgelist` - Integral vertex IDs + - `graph::edge_list::has_edge_value` - Concept for valued edges + - Type aliases: `edge_range_t`, `edge_iterator_t`, `edge_t`, `edge_reference_t`, `edge_value_t`, `vertex_id_t` + - Currently uses unqualified `source_id(uv)`, `target_id(uv)` - needs adjustment for unified CPOs 2. **`include/graph/graph_info.hpp`** - Shared types: - `edge_info` - Template specializations for edge projections - `edgelist_edge` - Alias for sourced edge info - `copyable_edge_t` - Lightweight copyable edge -3. **Reference implementation** at `/mnt/d/dev_graph/graph-v2/`: +3. **`include/graph/adj_list/detail/graph_cpo.hpp`** - CPO implementations: + - `source_id(g, uv)` - Four-tier resolution (native member, graph member, ADL, descriptor) + - `target_id(g, uv)` - Similar four-tier resolution + - `edge_value(g, uv)` - Similar pattern + - **Key issue**: These CPOs require a graph parameter `g`, but edge_list concepts expect `source_id(e)` without graph + +4. **Reference implementation** at `/mnt/d/dev_graph/graph-v2/`: - `include/graph/edgelist.hpp` - Edge list concepts, types, and CPOs (in `graph::edge_list` namespace) - `include/graph/views/edgelist.hpp` - Edgelist view for adjacency lists @@ -41,214 +58,427 @@ allowing algorithms to work directly with edge ranges. | Component | Current Status | Required | |-----------|---------------|----------| -| `graph::edge_list` namespace | Exists in graph-v2, not in v3 | ✅ Needed | -| Edge list concepts | Not in v3 | ✅ Needed | -| Edge list type aliases | Not in v3 | ✅ Needed | -| Edge list CPOs | Not in v3 | ✅ Needed | -| Edgelist view for adj_list | Not in v3 | ✅ Needed (future) | -| Descriptor support | Not in graph-v2 | ✅ Needs design | +| `graph::edge_list` namespace | ✅ Exists in v3 | Extend with CPO support | +| Edge list concepts | ✅ Exists in v3 | Adjust to use unified CPOs | +| Edge list type aliases | ✅ Exists in v3 | Complete | +| Unified CPO support | ❌ Missing | **Critical**: Extend CPOs for edge_list | +| Edge descriptor for edge_list | ❌ Missing | Design needed | +| `edgelist(g)` view | ❌ Missing | Future phase | ---- +### Key Design Challenge -## Implementation Strategy +The current architecture has a fundamental tension: -### Phase 1: Core Edge List Namespace +**Adjacency List CPOs**: `source_id(g, uv)` - requires graph context because: +- The edge descriptor needs the graph to resolve vertex references +- The graph provides the context for source/target resolution -**Goal**: Create the `graph::edge_list` namespace with concepts, types, and CPOs. +**Edge List CPOs**: `source_id(uv)` - edge-only because: +- Edge list elements are self-contained (source_id, target_id, value stored directly) +- No graph context needed -#### 1.1 Create `include/graph/edge_list/edge_list_concepts.hpp` +**Resolution Strategy**: We need a unified approach where both forms work: +- For adjacency lists: `source_id(g, uv)` remains the canonical form +- For edge lists: `source_id(el, uv)` where `el` is the edge_list container (or use sentinel) +- CPOs detect edge type and dispatch appropriately -Define edge list concepts modeled after the adjacency list pattern: +--- -```cpp -namespace graph::edge_list { +## Implementation Strategy -// Concept: An edge list is a range of edges with source_id and target_id -template -concept basic_sourced_edgelist = - std::ranges::input_range && - !std::ranges::range> && // distinguish from adj_list - requires(std::ranges::range_value_t e) { - { source_id(e) }; - { target_id(e) } -> std::same_as; - }; +### Phase 1: Unified CPO Architecture -template -concept basic_sourced_index_edgelist = - basic_sourced_edgelist && - requires(std::ranges::range_value_t e) { - { source_id(e) } -> std::integral; - }; +**Goal**: Extend existing CPOs to support edge_list values alongside adjacency list edge descriptors. -template -concept has_edge_value = - basic_sourced_edgelist && - requires(std::ranges::range_value_t e) { - { edge_value(e) }; - }; +#### 1.1 CPO Unification Design -} // namespace graph::edge_list +The unified CPO approach extends the existing `adj_list` CPOs to handle edge_list types: + +```cpp +// Current: source_id(g, uv) where uv is edge_descriptor +// Extended: source_id(g, uv) where uv can be: +// - adj_list::edge_descriptor (adjacency list edge) +// - edge_list::edge_descriptor (edge list descriptor) +// - edge_info (edge list value with source_id/target_id members) +// - tuple/pair (generic edge representation) +// - any type satisfying edge_list::basic_edge concept ``` -#### 1.2 Create `include/graph/edge_list/edge_list_types.hpp` +**CPO Resolution Order** (extended from current 4-tier to 7-tier): -Define type aliases for edge lists: +| Priority | Strategy | Concept Check | Description | +|----------|----------|---------------|-------------| +| 1 | Native edge member | `is_edge_descriptor_v` + `(*uv.value()).source_id()` | Native edge type member | +| 2 | Graph member | `g.source_id(uv)` exists | Graph's member function | +| 3 | ADL with graph | `source_id(g, uv)` exists | ADL-findable free function | +| 4 | adj_list descriptor | `is_edge_descriptor_v` + `uv.source_id()` | adj_list edge descriptor method | +| 5 | **edge_list descriptor** | `is_edge_list_descriptor_v` + `uv.source_id()` | edge_list descriptor method | +| 6 | **Edge-only data member** | `uv.source_id` exists (not callable) | Direct member access (edge_info) | +| 7 | **Tuple/pair access** | `!is_*_descriptor_v` + `get<0>(uv)` | For tuple-like edge types | -```cpp -namespace graph::edge_list { +**Key Design Considerations**: -template -using edge_range_t = EL; +1. **Descriptor Type Gates**: Tiers 1, 4-5 are gated by descriptor type traits to prevent + ambiguity. The `is_edge_descriptor_v` trait only matches `adj_list::edge_descriptor`, + while `is_edge_list_descriptor_v` only matches `edge_list::edge_descriptor`. -template -using edge_iterator_t = std::ranges::iterator_t>; +2. **Method vs Member Disambiguation**: + - Tier 4-5: `uv.source_id()` is a **method call** (requires parentheses) + - Tier 6: `uv.source_id` is a **data member access** (no parentheses) + - The concept checks distinguish these: `requires { uv.source_id(); }` vs `requires { uv.source_id; }` -template -using edge_t = std::ranges::range_value_t>; +3. **Fallback Order**: Tuple-like access (tier 7) is last to allow custom types with + `source_id` members to take precedence over generic tuple decomposition. -template -using edge_reference_t = std::ranges::range_reference_t>; +4. **noexcept propagation**: Each tier mirrors the noexcept of the expression it calls, matching + tiers 1–4 in the existing CPO; implement the same for tiers 5–7. -template -using vertex_id_t = decltype(source_id(std::declval>())); +5. **Mutual exclusivity guardrails**: Ensure tier 6 (data member) explicitly excludes any type that + satisfies either descriptor trait or has a callable `source_id()`/`target_id()`. Likewise, tier 7 + must exclude descriptor traits and edge_info-like types to avoid ambiguity. -template -using edge_value_t = decltype(edge_value(std::declval>())); +#### 1.2 Extend `graph_cpo.hpp` -} // namespace graph::edge_list +Add edge_list support to existing CPOs in `include/graph/adj_list/detail/graph_cpo.hpp`: + +```cpp +namespace _source_id { + enum class _St { + _none, + _native_edge_member, + _member, + _adl, + _adj_list_descriptor, // RENAMED: was _descriptor + _edge_list_descriptor, // NEW: for edge_list::edge_descriptor + _edge_info_member, // NEW: for edge_info + _tuple_like // NEW: for pair/tuple + }; + + // Existing: Check for adj_list edge descriptor (unchanged) + template + concept _has_adj_list_descriptor = is_edge_descriptor_v> && + requires(const UV& uv) { + { uv.source_id() }; // method call + }; + + // NEW: Check for edge_list edge descriptor + template + concept _has_edge_list_descriptor = + edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { uv.source_id() }; // method call + }; + + // NEW: Check for edge_info-style direct data member access + // Must NOT be a descriptor type (to avoid ambiguity with method calls) + template + concept _has_edge_info_member = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + uv.source_id; // data member, not method + } && + !requires(const UV& uv) { + uv.source_id(); // exclude if it's callable (i.e., a method) + }; + + // NEW: Check for tuple-like edge (pair, tuple) + // Must NOT be any descriptor type + template + concept _is_tuple_like_edge = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { std::get<0>(uv) }; + { std::get<1>(uv) }; + }; + + // Extended _Choose function adds new tiers after existing ones... + template + [[nodiscard]] consteval _Choice_t<_St> _Choose() noexcept { + if constexpr (_has_native_edge_member) { + return {_St::_native_edge_member, /*noexcept*/}; + } else if constexpr (_has_member) { + return {_St::_member, /*noexcept*/}; + } else if constexpr (_has_adl) { + return {_St::_adl, /*noexcept*/}; + } else if constexpr (_has_adj_list_descriptor) { + return {_St::_adj_list_descriptor, /*noexcept*/}; + } else if constexpr (_has_edge_list_descriptor) { // NEW + return {_St::_edge_list_descriptor, /*noexcept*/}; + } else if constexpr (_has_edge_info_member) { // NEW + return {_St::_edge_info_member, /*noexcept*/}; + } else if constexpr (_is_tuple_like_edge) { // NEW + return {_St::_tuple_like, /*noexcept*/}; + } else { + return {_St::_none, false}; + } + } +} ``` -#### 1.3 Create `include/graph/edge_list/edge_list_cpo.hpp` +**Symmetry requirement**: Apply the same tiering and trait gates to `target_id` and `edge_value` +to preserve unified behavior across all edge properties. -Define CPOs for edge lists (edge-level operations): +**API note**: When the context object is an edge_list container, `source_id(el, uv)` must not depend +on `el` at runtime; keep it a no-op parameter to remain noexcept-friendly and uniform with +adjacency-list usage. -```cpp -namespace graph::edge_list { +#### 1.3 Update Edge List Concepts -// CPO: source_id(e) - Get source vertex ID from edge -inline constexpr auto source_id = /* CPO implementation */; +Modify `include/graph/edge_list/edge_list.hpp` concepts to use unified CPOs: -// CPO: target_id(e) - Get target vertex ID from edge -inline constexpr auto target_id = /* CPO implementation */; +```cpp +namespace graph::edge_list { -// CPO: edge_value(e) - Get edge value (optional) -inline constexpr auto edge_value = /* CPO implementation */; +// Forward declare the unified CPO usage +// Edge list concepts now check if CPOs work with a "dummy" graph context -// Edge list-level CPOs -// CPO: num_edges(el) - Get number of edges -inline constexpr auto num_edges = /* CPO implementation */; +template +concept basic_sourced_edgelist = + std::ranges::input_range && + !std::ranges::range> && // distinguish from adj_list + requires(EL& el, std::ranges::range_value_t uv) { + // Use unified CPO with edge_list as context + { graph::source_id(el, uv) }; + { graph::target_id(el, uv) } -> std::same_as; + }; } // namespace graph::edge_list ``` -#### 1.4 Create `include/graph/edge_list/edge_list.hpp` - -Main header that includes all edge list components: - -```cpp -#pragma once - -#include "edge_list_concepts.hpp" -#include "edge_list_types.hpp" -#include "edge_list_cpo.hpp" -``` +**Alternative Design**: If the edge types are self-describing (have source_id/target_id members), +the graph parameter becomes a no-op context. This preserves API consistency. --- -### Phase 2: Edge List Container +### Phase 2: Edge Descriptor for Edge Lists -**Goal**: Refactor the existing `edgelist.hpp` to conform to the edge list concepts. +**Goal**: Define an edge descriptor type for edge lists that enables uniform algorithm usage. -#### 2.1 Refactor `include/graph/edgelist.hpp` +#### 2.1 Edge List Descriptor Design -Move the container to the edge_list namespace and ensure it satisfies concepts: +Unlike adjacency list edge descriptors (which wrap iterators), edge list descriptors are lightweight +reference wrappers: ```cpp namespace graph::edge_list { -template>> -class edgelist { - // ... existing implementation +/** + * @brief Lightweight edge descriptor for edge lists + * + * This descriptor is a non-owning reference to edge data stored in an edge list. + * It stores references to source ID, target ID, and optionally edge value, avoiding + * any copies. The descriptor is only valid as long as the referenced data exists. + * + * This is a true lightweight handle - construction is O(1) with zero copies. + */ +template +class edge_descriptor { +public: + using vertex_id_type = VId; + using edge_value_type = EV; + + // Constructor without value (for void EV) + constexpr edge_descriptor(const VId& src, const VId& tgt) + requires std::is_void_v; + + // Constructor with value (for non-void EV) + template + requires (!std::is_void_v) + constexpr edge_descriptor(const VId& src, const VId& tgt, const E& val); - // Ensure CPO customization points work - friend constexpr VId source_id(const edge& e) noexcept { return e.source; } - friend constexpr VId target_id(const edge& e) noexcept { return e.target; } - // ... etc + // Accessors return references to the underlying data + [[nodiscard]] constexpr const VId& source_id() const noexcept; + [[nodiscard]] constexpr const VId& target_id() const noexcept; + + // Value accessor (only for non-void EV) + template + requires (!std::is_void_v) + [[nodiscard]] constexpr const E& value() const noexcept; + + // Comparison operators compare values, not reference addresses + constexpr bool operator==(const edge_descriptor& other) const noexcept; + constexpr auto operator<=>(const edge_descriptor& other) const noexcept; + +private: + const VId& source_id_; + const VId& target_id_; + [[no_unique_address]] std::conditional_t, + detail::empty_value, std::reference_wrapper> value_; }; +// Type trait to distinguish edge_list descriptors +template +struct is_edge_list_descriptor : std::false_type {}; + +template +struct is_edge_list_descriptor> : std::true_type {}; + +template +inline constexpr bool is_edge_list_descriptor_v = is_edge_list_descriptor::value; + } // namespace graph::edge_list ``` -#### 2.2 Add Descriptor Support +**Key Design Properties**: +- **Zero-copy**: All data members are references (`const VId&`, `std::reference_wrapper`) +- **Lightweight**: Descriptor size is ~3 pointers (24 bytes on 64-bit systems) +- **Reference semantics**: Changes to underlying data are visible through descriptor +- **No ownership**: Descriptor lifetime is independent of data lifetime (user responsibility) +- **Encapsulated**: Class with private members and public interface -Design consideration: Edge lists need descriptor support for consistency with v3. +#### 2.2 CPO Support for Edge List Descriptors -```cpp -namespace graph::edge_list { +The unified CPOs detect edge_list descriptors via the type trait and dispatch to the method call: -// Edge descriptor for edge lists -template -struct edge_descriptor { - VId source_id; - VId target_id; - // Optional: pointer/iterator to underlying edge for value access -}; +```cpp +// In graph_cpo.hpp _source_id namespace + +// Tier 5: edge_list descriptor (method call, like adj_list but different trait) +template +concept _has_edge_list_descriptor = + edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { uv.source_id() }; // method call with parentheses + }; -} // namespace graph::edge_list +// In _fn::operator(): +// else if constexpr (_Choice<_G, _UV>._Strategy == _St::_edge_list_descriptor) { +// return uv.source_id(); // method call +// } ``` +**Note**: Both `adj_list::edge_descriptor` and `edge_list::edge_descriptor` use method syntax +(`uv.source_id()`), but they are distinguished by their respective type traits. This allows +clean separation while maintaining consistent API. + --- -### Phase 3: Default CPO Implementations +### Phase 3: Support Standard Edge Types + +**Goal**: Provide out-of-box support for common edge representations. -**Goal**: Provide default implementations for common edge types. +#### 3.1 Supported Types Matrix -#### 3.1 Support Standard Types +| Type | Tier | `source_id(g,uv)` | `target_id(g,uv)` | `edge_value(g,uv)` | +|------|------|-------------------|-------------------|--------------------| +| `adj_list::edge_descriptor` (native edge has `source_id()`) | 1 | `(*uv.value()).source_id()` | `(*uv.value()).target_id()` | via native edge | +| Custom types (graph member or ADL) | 2-3 | `g.source_id(uv)` or ADL | `g.target_id(uv)` or ADL | `g.edge_value(uv)` or ADL | +| `adj_list::edge_descriptor` (default) | 4 | `uv.source_id()` | `uv.target_id()` | `uv.value()` | +| `edge_list::edge_descriptor` | 5 | `uv.source_id()` | `uv.target_id()` | `uv.value()` | +| `edge_info` | 6 | `uv.source_id` | `uv.target_id` | N/A | +| `edge_info` | 6 | `uv.source_id` | `uv.target_id` | `uv.value` | +| `edge_info` | 6 | `uv.source_id` | `uv.target_id` | N/A | +| `edge_info` | 6 | `uv.source_id` | `uv.target_id` | `uv.value` | +| `pair` | 7 | `uv.first` | `uv.second` | N/A | +| `tuple` | 7 | `get<0>(uv)` | `get<1>(uv)` | N/A | +| `tuple` | 7 | `get<0>(uv)` | `get<1>(uv)` | `get<2>(uv)` | +| `tuple` | 7 | `get<0>(uv)` | `get<1>(uv)` | `get<2>(uv)` | -The following types should work with edge list CPOs out-of-the-box: +**Note**: Tier 1 applies when an `adj_list::edge_descriptor` wraps a native edge type that provides +its own `source_id()`/`target_id()` methods. The CPO dereferences the descriptor's iterator +(`*uv.value()`) to access the native edge's member. This is highest priority because it honors +the underlying data structure's semantics. -| Type | `source_id(e)` | `target_id(e)` | `edge_value(e)` | -|------|----------------|----------------|-----------------| -| `pair` | `e.first` | `e.second` | N/A | -| `tuple` | `get<0>(e)` | `get<1>(e)` | N/A | -| `tuple` | `get<0>(e)` | `get<1>(e)` | `get<2>(e)` | -| `edge_info` | `e.source_id` | `e.target_id` | N/A | -| `edge_info` | `e.source_id` | `e.target_id` | `e.value` | -| `edge` | `e.source` | `e.target` | N/A | -| `edge` | `e.source` | `e.target` | `e.value` | +#### 3.2 Implementation in CPO - Tier 6 and 7 + +```cpp +namespace _source_id { + // Tier 6: edge_info-style data member access + // Excludes descriptor types to avoid ambiguity with method calls + template + concept _has_edge_info_member = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + uv.source_id; // data member access (no parens) + } && + !requires(const UV& uv) { + uv.source_id(); // must NOT be callable + }; + + // Tier 7: Tuple-like detection and access + // Excludes all descriptor types + template + concept _is_tuple_like_edge = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { std::tuple_size_v> } -> std::convertible_to; + { std::get<0>(uv) }; + }; + + // In _fn::operator(): + // ... tiers 1-5 ... + // else if constexpr (_Choice<_G, _UV>._Strategy == _St::_edge_info_member) { + // return uv.source_id; // data member + // } else if constexpr (_Choice<_G, _UV>._Strategy == _St::_tuple_like) { + // return std::get<0>(uv); + // } +} +``` --- -### Phase 4: Edgelist Views (Future) +### Phase 4: Edgelist View (Future) -**Goal**: Create views that iterate over all edges in an adjacency list. +**Goal**: Create a view that presents an adjacency list as an edge list. -#### 4.1 Create `include/graph/views/edgelist_view.hpp` +#### 4.1 Edgelist View Design ```cpp namespace graph::views { -// edgelist(g) - Iterate all edges in adjacency list as edge_info +/** + * @brief View that iterates over all edges in an adjacency list + * + * Transforms an adjacency list into an edge list view, allowing algorithms + * designed for edge lists to work with adjacency lists. + */ template -class edgelist_view { - // Iterator that walks vertices and their edges - // Projects to edge_info +class edgelist_view : public std::ranges::view_interface> { +public: + class iterator { + // Iterates through vertices, then through each vertex's edges + // Projects each edge to edge_info, void> + }; + + explicit edgelist_view(G& g) : g_(&g) {} + + iterator begin() const; + std::default_sentinel_t end() const { return {}; } + +private: + G* g_; }; -// edgelist(g, evf) - With edge value function +// With value projection template -class edgelist_view { - // Projects to edge_info -}; +class edgelist_view_with_value; -// CPO: edgelist(g) and edgelist(g, evf) -inline constexpr auto edgelist = /* range adaptor */; +// CPO +inline constexpr auto edgelist = /* range adaptor closure */; } // namespace graph::views ``` +#### 4.2 Algorithm Compatibility + +With the unified CPO design, algorithms can accept both: + +```cpp +template + requires edge_list::basic_sourced_edgelist +void some_edge_algorithm(EdgeRange&& edges) { + for (auto&& uv : edges) { + auto uid = graph::source_id(edges, uv); // Works for edge_list + auto vid = graph::target_id(edges, uv); // Works for edgelist view + // ... + } +} +``` + --- ## File Structure @@ -256,74 +486,129 @@ inline constexpr auto edgelist = /* range adaptor */; ``` include/graph/ ├── edge_list/ -│ ├── edge_list.hpp # Main include header -│ ├── edge_list_concepts.hpp # Concepts -│ ├── edge_list_types.hpp # Type aliases -│ ├── edge_list_cpo.hpp # CPO definitions -│ └── edge_list_container.hpp # edgelist container +│ ├── edge_list.hpp # Main header (existing, to be updated) +│ ├── edge_list_descriptor.hpp # NEW: Edge list descriptor type +│ └── detail/ +│ └── edge_list_cpo.hpp # NEW: Edge list CPO helpers (if needed) ├── views/ -│ └── edgelist_view.hpp # Edgelist view for adj_list (Phase 4) -├── edgelist.hpp # Backward compat → includes edge_list/ +│ └── edgelist_view.hpp # NEW: Edgelist view for adj_list (Phase 4) +├── adj_list/ +│ └── detail/ +│ └── graph_cpo.hpp # MODIFY: Extend CPOs for edge_list support ├── graph_info.hpp # (existing) shared edge_info types -└── graph.hpp # Update to import edge_list namespace +└── graph.hpp # MODIFY: Update imports, remove edgelist.hpp ref ``` --- ## Implementation Order -### Milestone 1: Core Framework -1. [ ] Create `include/graph/edge_list/` directory -2. [ ] Implement `edge_list_concepts.hpp` -3. [ ] Implement `edge_list_types.hpp` -4. [ ] Implement `edge_list_cpo.hpp` with CPO infrastructure - -### Milestone 2: Container Integration -5. [ ] Refactor existing `edgelist.hpp` into `edge_list_container.hpp` -6. [ ] Add friend functions for CPO customization -7. [ ] Create backward-compatible `edgelist.hpp` wrapper -8. [ ] Design and implement edge descriptors - -### Milestone 3: Testing -9. [ ] Create `tests/test_edge_list_concepts.cpp` -10. [ ] Create `tests/test_edge_list_cpo.cpp` -11. [ ] Test with standard types (pair, tuple) -12. [ ] Test with custom edge types - -### Milestone 4: Edgelist Views (Future) -13. [ ] Implement `edgelist_view.hpp` -14. [ ] Create edgelist view CPO -15. [ ] Test edgelist view with adjacency list graphs +### Milestone 1: CPO Unification (Critical Path) +1. [ ] Add type traits for edge_list types in descriptor_traits.hpp or new file +2. [ ] Extend `source_id` CPO in `graph_cpo.hpp` to support edge_info and tuple types +3. [ ] Extend `target_id` CPO in `graph_cpo.hpp` to support edge_info and tuple types +4. [ ] Extend `edge_value` CPO in `graph_cpo.hpp` to support edge_info and tuple types + +### Milestone 2: Edge List Descriptor +5. [ ] Create `include/graph/edge_list/edge_list_descriptor.hpp` +6. [ ] Add `is_edge_list_descriptor` type trait +7. [ ] Integrate descriptor support into CPOs + +### Milestone 3: Update Edge List Concepts +8. [ ] Update `edge_list.hpp` concepts to use unified CPO pattern +9. [ ] Ensure backward compatibility with existing code +10. [ ] Update `graph.hpp` to properly include edge_list (remove edgelist.hpp reference) + +### Milestone 4: Testing +11. [ ] Create `tests/edge_list/test_edge_list_concepts.cpp` +12. [ ] Create `tests/edge_list/test_edge_list_cpo.cpp` +13. [ ] Test CPOs with standard types (pair, tuple, edge_info) +14. [ ] Test CPOs with custom edge types via ADL + +### Milestone 5: Edgelist View (Future) +15. [ ] Implement `edgelist_view.hpp` +16. [ ] Create `edgelist` range adaptor CPO +17. [ ] Test edgelist view with adjacency list graphs +18. [ ] Verify algorithm compatibility across edge sources --- ## Design Decisions -### Decision 1: CPO Architecture +### Decision 1: CPO Architecture - Unified vs Separate + +**Options**: +- A) Separate CPOs: `graph::adj_list::source_id(g,e)` and `graph::edge_list::source_id(e)` +- B) Unified CPOs: Single `graph::source_id(g,e)` that works with all edge types +- C) Overloaded CPOs: Both `source_id(g,e)` and `source_id(e)` forms in single namespace + +**Decision**: Option B - Unified CPOs + +**Rationale**: +- Algorithms can be written once to work with any edge source +- Maintains API consistency with existing adjacency list CPOs +- The graph/edge_list parameter provides context even when not strictly needed +- Follows the principle of least surprise for library users + +### Decision 2: Graph Parameter for Edge Lists **Options**: -- A) Follow `adj_list` pattern with `_cpo_impls` namespace -- B) Simpler direct CPO implementation -- C) Niebloid-style function objects +- A) Require graph parameter: `source_id(el, uv)` where `el` is the edge list container +- B) Optional graph parameter: `source_id(uv)` when edge is self-describing +- C) Tag dispatch: Use `source_id(edge_list_tag{}, uv)` for edge lists + +**Decision**: Option A - Require graph parameter (edge list container) -**Recommendation**: Option A - Follow `adj_list` pattern for consistency. +**Rationale**: +- Maintains consistent API: `source_id(context, edge)` everywhere +- For self-describing edges, the context is simply ignored by the CPO +- Enables future extensions (e.g., edge list with external vertex mapping) +- Algorithms written for adjacency lists work with edge lists with minimal changes -### Decision 2: Descriptor Support +### Decision 3: Edge Descriptor Design **Options**: -- A) No descriptors (like graph-v2) -- B) Lightweight edge descriptors (VId pair only) -- C) Full descriptor support with edge reference +- A) No descriptors for edge lists (edges are self-contained values) +- B) Lightweight value-based descriptors (copy of source_id, target_id, optional value) +- C) Iterator-based descriptors (like adjacency list edge descriptors) + +**Decision**: Option B - Lightweight value-based descriptors -**Recommendation**: Option B - Start lightweight, extend as needed. +**Rationale**: +- Edge list edges are already value types, so descriptors are essentially wrappers +- Provides uniform descriptor interface for algorithm compatibility +- Avoids iterator invalidation issues +- Simple implementation with good performance characteristics -### Decision 3: Namespace Organization +### Decision 4: Namespace Organization **Options**: -- A) Everything in `graph::edge_list` -- B) Split: `graph::edge_list` for types, `graph::edge_list::views` for views +- A) Everything in `graph::edge_list`, views in `graph::edge_list::views` +- B) Types/concepts in `graph::edge_list`, views in `graph::views` +- C) All edge_list items exported to `graph::` namespace (like adj_list) -**Recommendation**: Option A initially, refactor if needed. +**Decision**: Option B with selective export to `graph::` + +**Rationale**: +- Keeps implementation organized in dedicated namespaces +- `graph::views::edgelist` parallels `graph::views::vertices` pattern +- Common types/CPOs exported to `graph::` for convenience +- Matches existing `adj_list` namespace organization + +### Decision 5: Concept Constraint Style + +**Options**: +- A) Concepts require specific member names (e.g., `uv.source_id`) +- B) Concepts require CPO expressions (e.g., `graph::source_id(el, uv)`) +- C) Concepts use both approaches with fallback + +**Decision**: Option B - Concepts require CPO expressions + +**Rationale**: +- CPO-based concepts allow any customization mechanism to work +- More flexible for user-defined edge types +- Matches modern C++ library design patterns +- Provides consistent constraint style across the library --- @@ -331,30 +616,51 @@ include/graph/ ### Unit Tests -1. **Concept satisfaction tests**: - - `static_assert(basic_sourced_edgelist>>)` - - `static_assert(basic_sourced_edgelist>)` - -2. **CPO tests**: - - Test `source_id`, `target_id`, `edge_value` with various types - - Test `num_edges` with edge list containers - -3. **Container tests**: - - Add/remove edges - - Iteration - - Size queries +1. **CPO Tests with Various Edge Types**: + - `source_id(el, pair)` → returns `uv.first` + - `source_id(el, tuple)` → returns `get<0>(uv)` + - `source_id(el, edge_info)` → returns `uv.source_id` + - `source_id(g, edge_descriptor)` → returns via descriptor (existing) + - Ambiguity guards: + - Type with both `source_id()` method and `source_id` data member → picks method tier + - Tuple-like type that also has `source_id` data member → picks data-member tier, not tuple + - `edge_list::edge_descriptor` vs `adj_list::edge_descriptor` → verify distinct tiers + +2. **Concept Satisfaction Tests**: + ```cpp + static_assert(edge_list::basic_sourced_edgelist>>); + static_assert(edge_list::basic_sourced_edgelist>>); + static_assert(!edge_list::basic_sourced_edgelist); // fails range-of-non-range check + ``` + +3. **Descriptor Tests**: + - Construct edge_list::edge_descriptor from edge_info + - Verify CPOs work with edge_list descriptors + - Test equality, comparison, hashing + +4. **Integration with Existing CPOs**: + - Verify `adj_list` CPOs still work unchanged + - Test both edge types in same compilation unit ### Integration Tests -1. Algorithm compatibility (future) -2. Conversion between edge list and adjacency list (future) +1. **Algorithm Compatibility** (future): + - Simple edge iteration algorithm works with edge_list + - Same algorithm works with edgelist view over adjacency list + +2. **Conversion Tests** (future): + - Build adjacency list from edge_list + - Create edgelist view from adjacency list, verify iteration --- ## References -- [edge_list_goal.md](edge_list_goal.md) - Goals document +- [edge_list_goal.md](edge_list_goal.md) - Goals document (revised) - [graph_cpo_implementation.md](../docs/graph_cpo_implementation.md) - CPO patterns +- [graph_cpo.hpp](../include/graph/adj_list/detail/graph_cpo.hpp) - Existing CPO implementations +- [edge_list.hpp](../include/graph/edge_list/edge_list.hpp) - Current edge list concepts +- [graph_info.hpp](../include/graph/graph_info.hpp) - Shared edge_info types - `/mnt/d/dev_graph/graph-v2/include/graph/edgelist.hpp` - Reference implementation - `/mnt/d/dev_graph/graph-v2/include/graph/views/edgelist.hpp` - Reference view implementation @@ -365,3 +671,4 @@ include/graph/ | Date | Version | Changes | |------|---------|---------| | 2026-01-25 | 1.0 | Initial strategy document | +| 2026-01-31 | 2.0 | Major revision: Unified CPO architecture, updated for revised goals | diff --git a/agents/view_goal.md b/agents/view_goal.md new file mode 100644 index 0000000..bcd0c87 --- /dev/null +++ b/agents/view_goal.md @@ -0,0 +1,5 @@ +# Create views for common graph traversals + +The directory at `/mnt/d/dev_graph/P1709` has documents describing different aspects of the graph library. +Use the `D3129/` subdirectory has latex and source examples that describe the design of the views that are desired. + diff --git a/agents/view_plan.md b/agents/view_plan.md new file mode 100644 index 0000000..ed155ba --- /dev/null +++ b/agents/view_plan.md @@ -0,0 +1,2020 @@ +# Graph Views Implementation Plan + +**Branch**: `feature/views-implementation` +**Based on**: [view_strategy.md](view_strategy.md) +**Status**: Phase 6 Complete (2026-02-01) + +--- + +## Overview + +This plan implements graph views as described in D3129 and detailed in view_strategy.md. Views provide lazy, range-based access to graph elements using structured bindings. Implementation is broken into discrete steps, each with tests and progress tracking. + +### Key Design Principles +- Value functions receive descriptors (not underlying values) +- Info structs use `void` template parameters for optional members +- Primary pattern: `VId=void` with descriptors (IDs accessible via descriptor) +- Edge descriptors contain source vertex descriptor as member (source_id always retrievable) +- No separate "sourced" edge views needed - source accessible via edge descriptor's source_id() +- Views are lazy, zero-copy where possible + +--- + +## Progress Tracking + +### Phase 0: Info Struct Refactoring ✅ (2026-01-31) +- [x] **Step 0.1**: Refactor vertex_info (all members optional via void) +- [x] **Step 0.2**: Refactor edge_info (all members optional via void) +- [x] **Step 0.3**: Refactor neighbor_info (all members optional via void) + +### Phase 1: Foundation +- [x] **Step 1.1**: Create directory structure ✅ (2026-02-01) +- [x] **Step 1.2**: Implement search_base.hpp (cancel_search, visited_tracker) ✅ (2026-02-01) +- [x] **Step 1.3**: Create view_concepts.hpp ✅ (2026-02-01) + +### Phase 2: Basic Views +- [x] **Step 2.1**: Implement vertexlist view + tests ✅ (2026-02-01) +- [x] **Step 2.2**: Implement incidence view + tests ✅ (2026-02-01) +- [x] **Step 2.3**: Implement neighbors view + tests ✅ (2026-02-01) +- [x] **Step 2.4**: Implement edgelist view for adjacency_list + tests ✅ (2026-02-01) +- [x] **Step 2.4.1**: Implement edgelist view for edge_list + tests ✅ (2026-02-01) +- [x] **Step 2.5**: Create basic_views.hpp header ✅ (2026-02-01) + +### Phase 3: DFS Views +- [x] **Step 3.1**: Implement DFS infrastructure + vertices_dfs + tests ✅ (2026-02-01) + - Accept both vertex_id and vertex_descriptor as seed parameter + - vertex_id constructor delegates to vertex_descriptor constructor +- [x] **Step 3.2**: Implement edges_dfs + tests ✅ (2026-02-01) + - Accept both vertex_id and vertex_descriptor as seed parameter +- [x] **Step 3.3**: Test DFS cancel functionality ✅ (2026-02-01) + +### Phase 4: BFS Views +- [x] **Step 4.1**: Implement BFS infrastructure + vertices_bfs + tests ✅ (2026-02-01) + - Accept both vertex_id and vertex_descriptor as seed parameter + - vertex_id constructor delegates to vertex_descriptor constructor +- [x] **Step 4.2**: Implement edges_bfs + tests ✅ (2026-02-01) + - Accept both vertex_id and vertex_descriptor as seed parameter +- [x] **Step 4.3**: Test BFS depth/size accessors ✅ (2026-02-01) + +### Phase 5: Topological Sort Views +- [x] **Step 5.1**: Implement topological sort algorithm + vertices_topological_sort + tests ✅ (2026-02-01) + - No seed parameter required (processes entire graph) + - Supports optional value function and allocator +- [x] **Step 5.2**: Implement edges_topological_sort + tests ✅ (2026-02-01) + - No seed parameter required (processes entire graph) +- [x] **Step 5.3**: Test cycle detection ✅ (2026-02-01) + - Safe variants with cycle detection implemented + +### Phase 6: Range Adaptors ✅ (2026-02-01) +- [x] **Step 6.1**: Implement range adaptor closures for basic views ✅ (2026-02-01) +- [x] **Step 6.2**: Implement range adaptor closures for search views ✅ (2026-02-01) + - Includes topological sort adaptors +- [x] **Step 6.3**: Test pipe syntax and chaining ✅ (2026-02-01) + +### Phase 7: Integration & Polish +- [x] **Step 7.1**: Create unified views.hpp header ✅ (2026-02-01) +- [x] **Step 7.2**: Update graph.hpp to include views ✅ (2026-02-01) +- [x] **Step 7.3**: Write documentation ✅ (2026-02-01) +- [x] **Step 7.4**: Performance benchmarks ✅ (2026-02-01) +- [x] **Step 7.5**: Edge case testing ✅ (2026-02-01) + +--- + +## Phase 0: Info Struct Refactoring ✅ COMPLETE + +**Completion Date**: 2026-01-31 +**Commit**: 330c7d8 "[views] Phase 0: Info struct refactoring complete" +**Test Results**: ✅ 27 test cases, 392 assertions, all passing + +**Implementation Summary**: +- Added 20 new VId=void specializations (4 vertex_info, 8 edge_info, 8 neighbor_info) +- Total specializations: 40 (8 vertex_info, 16 edge_info, 16 neighbor_info) +- All void template parameters physically omit corresponding members +- Comprehensive test suite created in `tests/views/` + +**Key Implementation Details Discovered**: +1. **edge_info member naming**: Uses `source_id` and `target_id` (vertex IDs), NOT `edge_id` +2. **neighbor_info member naming**: + - Uses `source_id` and `target_id` (vertex IDs), NOT `vertex_id` + - Uses `target` member when VId is present + - Uses `vertex` member when VId=void (descriptor-based pattern) +3. **Sourced parameter behavior**: + - `Sourced=true`: Includes `source_id` member (when VId present) + - `Sourced=false`: Omits `source_id` member +4. **Padding considerations**: sizeof tests account for struct padding + +--- + +### Step 0.1: Refactor vertex_info ✅ COMPLETE + +**Goal**: Make all members of vertex_info optional via void template parameters. + +**Files to Modify**: +- `include/graph/graph_info.hpp` + +**Implementation**: +```cpp +// Primary template - all members present +template +struct vertex_info { + using id_type = VId; + using vertex_type = V; + using value_type = VV; + + id_type id; + vertex_type vertex; + value_type value; +}; + +// Specializations for void combinations (8 total: 2^3) +// Example specialization: VId=void +template +struct vertex_info { + using id_type = void; + using vertex_type = V; + using value_type = VV; + + vertex_type vertex; + value_type value; + // No 'id' member +}; + +// Example specialization: VId=void, VV=void +template +struct vertex_info { + using id_type = void; + using vertex_type = V; + using value_type = void; + + vertex_type vertex; + // No 'id' or 'value' members +}; + +// ... 6 more specializations for other void combinations +// Key: Members are physically absent when their type parameter is void +``` + +**Tests to Create**: +- `tests/views/test_vertex_info.cpp` + - Test all 8 specializations compile + - Test structured bindings for each variant + - Test `vertex_info, int>` pattern + - Test `vertex_info` for external data + - Test copyability and movability + +**Acceptance Criteria**: +- All 8 specializations compile without errors +- Structured bindings work for all variants +- Void template parameters result in members being physically absent (not just zero-sized) +- `sizeof()` confirms space savings for void specializations +- Tests pass with sanitizers + +**Status**: ✅ COMPLETE + +**Commit Message**: +``` +[views] Refactor vertex_info: all members optional via void + +- VId, V, VV can all be void to suppress corresponding members +- Primary pattern: vertex_info +- External data pattern: vertex_info +- Add 8 specializations for void combinations +- Tests cover all variants and structured bindings +``` + +--- + +### Step 0.2: Refactor edge_info ✅ COMPLETE + +**Goal**: Make all members of edge_info optional via void template parameters. + +**Status**: ✅ COMPLETE + +**Files to Modify**: +- `include/graph/graph_info.hpp` + +**Implementation**: +```cpp +// Primary template - all members present +template +struct edge_info { + using source_id_type = conditional_t; + using target_id_type = VId; + using edge_type = E; + using value_type = EV; + + source_id_type source_id; // Present only when Sourced==true + target_id_type target_id; + edge_type edge; + value_type value; +}; + +// Example specialization: VId=void (suppresses source_id/target_id) +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = E; + using value_type = EV; + + edge_type edge; + value_type value; + // No source_id or target_id members +}; + +// Example specialization: VId=void, EV=void +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = E; + using value_type = void; + + edge_type edge; + // No source_id, target_id, or value members +}; + +// ... 14 more specializations for Sourced × void combinations (16 total: 2 × 2^3) +// Key: source_id only present when Sourced==true AND VId != void +``` + +**Tests to Create**: +- `tests/views/test_edge_info.cpp` + - Test all 16 specializations compile + - Test structured bindings for each variant + - Test `edge_info, EV>` pattern + - Test `edge_info` for external data + - Test Sourced=true vs Sourced=false behavior + - Test copyability and movability + +**Acceptance Criteria**: +- All specializations compile without errors +- Structured bindings work for all variants +- Sourced bool correctly controls source_id presence +- Tests pass with sanitizers + +**Commit Message**: +``` +[views] Refactor edge_info: all members optional via void + +- VId, E, EV can all be void to suppress corresponding members +- Sourced bool controls source_id presence (when VId != void) +- Primary pattern: edge_info +- External data pattern: edge_info +- Add 16 specializations for Sourced × void combinations +- Tests cover all variants and structured bindings +``` + +--- + +### Step 0.3: Refactor neighbor_info ✅ COMPLETE + +**Goal**: Make all members of neighbor_info optional via void template parameters. + +**Status**: ✅ COMPLETE + +**Implementation Note**: The actual implementation uses `target` as the member name when VId is present, and `vertex` when VId=void. This differs from the original plan which assumed consistent naming. + +**Files to Modify**: +- `include/graph/graph_info.hpp` + +**Implementation**: +```cpp +// Primary template - all members present +template +struct neighbor_info { + using source_id_type = conditional_t; + using target_id_type = VId; + using vertex_type = V; + using value_type = VV; + + source_id_type source_id; // Present only when Sourced==true + target_id_type target_id; + vertex_type vertex; + value_type value; +}; + +// Example specialization: VId=void (suppresses source_id/target_id) +// ACTUAL IMPLEMENTATION: member named 'vertex' when VId=void +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = V; + using value_type = VV; + + vertex_type vertex; // NOTE: 'vertex' not 'target' when VId=void + value_type value; + // No source_id or target_id members +}; + +// Example specialization: VId=void, VV=void (primary pattern for neighbors) +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = V; + using value_type = void; + + vertex_type vertex; // NOTE: 'vertex' not 'target' when VId=void + // No source_id, target_id, or value members +}; + +// ... 14 more specializations for Sourced × void combinations (16 total: 2 × 2^3) +// Key: source_id only present when Sourced==true AND VId != void +``` + +**Tests to Create**: +- `tests/views/test_neighbor_info.cpp` + - Test all 16 specializations compile + - Test structured bindings for each variant + - Test `neighbor_info, VV>` pattern + - Test `neighbor_info` for external data + - Test Sourced=true vs Sourced=false behavior + - Test copyability and movability + +**Acceptance Criteria**: +- All specializations compile without errors +- Structured bindings work for all variants +- Primary pattern yields {vertex, value} for neighbor iteration +- Tests pass with sanitizers + +**Commit Message**: +``` +[views] Refactor neighbor_info: all members optional via void + +- VId, V, VV can all be void to suppress corresponding members +- Sourced bool controls source_id presence (when VId != void) +- Primary pattern: neighbor_info +- External data pattern: neighbor_info +- Add 16 specializations for Sourced × void combinations +- Tests cover all variants and structured bindings +``` + +--- + +## Phase 1: Foundation + +### Step 1.1: Create directory structure + +**Goal**: Set up the directory structure for view implementation. + +**Files to Create**: +- `include/graph/views/` (directory) +- `tests/views/` (directory) ✅ DONE in Phase 0 +- `tests/views/CMakeLists.txt` ✅ DONE in Phase 0 + +**Note**: `tests/views/` directory and CMakeLists.txt already created during Phase 0. + +**Implementation**: +```cmake +# tests/views/CMakeLists.txt +add_executable(graph3_views_tests + test_main.cpp + test_vertex_info.cpp + test_edge_info.cpp + test_neighbor_info.cpp +) + +target_link_libraries(graph3_views_tests + PRIVATE + graph3::graph3 + Catch2::Catch2 +) + +add_test(NAME views_tests COMMAND graph3_views_tests) +``` + +**Tests to Create**: +- `tests/views/test_main.cpp` (Catch2 main) + +**Acceptance Criteria**: +- Directories created +- CMakeLists.txt properly configured +- Test executable builds and runs (even if empty) +- Integrated with parent CMakeLists.txt + +**Status**: ✅ COMPLETE (2026-02-01) + +**Implementation Notes**: +- `include/graph/views/` directory created +- `tests/views/` directory and infrastructure already existed from Phase 0 +- CMakeLists.txt already configured with test files from Phase 0 +- Test executable already builds and runs with Phase 0 tests + +**Commit Message**: +``` +[views] Phase 1.1: Create views directory structure + +- Add include/graph/views/ directory for view implementations +- tests/views/ infrastructure already completed in Phase 0 +``` + +--- + +### Step 1.2: Implement search_base.hpp + +**Goal**: Implement common search infrastructure (cancel_search, visited_tracker). + +**Files to Create**: +- `include/graph/views/search_base.hpp` + +**Implementation**: +```cpp +namespace graph::views { + +// Search cancellation control +enum class cancel_search { + continue_search, // Continue normal traversal + cancel_branch, // Skip subtree, continue with siblings + cancel_all // Stop entire search +}; + +// Visited tracking for search views +template> +class visited_tracker { + std::vector visited_; +public: + explicit visited_tracker(std::size_t num_vertices, Alloc alloc = {}) + : visited_(num_vertices, false, alloc) {} + + bool is_visited(VId id) const { + return visited_[static_cast(id)]; + } + + void mark_visited(VId id) { + visited_[static_cast(id)] = true; + } + + void reset() { + std::ranges::fill(visited_, false); + } + + std::size_t size() const { return visited_.size(); } +}; + +} // namespace graph::views +``` + +**Tests to Create**: +- `tests/views/test_search_base.cpp` + - Test cancel_search enum values + - Test visited_tracker with various VId types (size_t, int, etc.) + - Test mark_visited and is_visited correctness + - Test reset functionality + - Test custom allocator support + +**Acceptance Criteria**: +- cancel_search enum compiles and is usable +- visited_tracker works with size_t and other integral types +- Tests cover edge cases (empty tracker, all visited, etc.) +- Custom allocator support verified + +**Status**: ✅ COMPLETE (2026-02-01) + +**Implementation Notes**: +- Created `include/graph/views/search_base.hpp` with cancel_search enum and visited_tracker +- Used `std::fill` instead of `std::ranges::fill` for vector compatibility +- Added comprehensive tests covering all functionality and edge cases +- All 61 assertions in 6 test cases passing + +**Commit Message**: +``` +[views] Phase 1.2: Implement search_base.hpp infrastructure + +- Add cancel_search enum for traversal control +- Implement visited_tracker template for DFS/BFS +- Support custom allocators for visited storage +- Tests verify correctness and edge cases (61 assertions) +``` + +--- + +### Step 1.3: Create view_concepts.hpp + +**Goal**: Define concepts specific to view implementation (if needed beyond existing graph concepts). + +**Files to Create**: +- `include/graph/views/view_concepts.hpp` + +**Implementation**: +```cpp +namespace graph::views { + +// Concept for types that can be used as value functions +template +concept vertex_value_function = + std::invocable && + (!std::is_void_v>); + +template +concept edge_value_function = + std::invocable && + (!std::is_void_v>); + +// Concept for search views (has depth, size, cancel) +template +concept search_view = requires(V& v, const V& cv) { + { v.cancel() } -> std::convertible_to; + { cv.depth() } -> std::convertible_to; + { cv.size() } -> std::convertible_to; +}; + +} // namespace graph::views +``` + +**Tests to Create**: +- `tests/views/test_view_concepts.cpp` + - Test vertex_value_function concept with valid/invalid types + - Test edge_value_function concept with valid/invalid types + - Test search_view concept (will be used later with actual views) + +**Acceptance Criteria**: +- Concepts compile and correctly constrain types +- Static assertions pass for valid/invalid cases +- Concepts integrate with existing graph concepts + +**Status**: ✅ COMPLETE (2026-02-01) + +**Implementation Notes**: +- Created `include/graph/views/view_concepts.hpp` with three key concepts: + - `vertex_value_function` - constrains vertex value functions + - `edge_value_function` - constrains edge value functions + - `search_view` - constrains search views (requires cancel(), depth(), size()) +- Comprehensive test suite with static assertions and runtime tests +- All 27 assertions in 4 test cases passing +- Tests cover valid/invalid types, different return types, mutable/capturing lambdas + +**Commit Message**: +``` +[views] Phase 1.3: Add view_concepts.hpp + +- Define vertex_value_function and edge_value_function concepts +- Define search_view concept for DFS/BFS/topo views +- Comprehensive tests verify concept constraints (27 assertions) +- Phase 1 (Foundation) complete +``` + +--- + +## Phase 2: Basic Views + +### Step 2.1: Implement vertexlist view ✅ COMPLETE + +**Status**: Implemented and tested (83 assertions, 14 test cases) + +**Files Created**: +- `include/graph/views/vertexlist.hpp` - View implementation +- `tests/views/test_vertexlist.cpp` - Comprehensive test suite + +**Implementation Summary**: +- `vertexlist_view` - No value function variant +- `vertexlist_view` - With value function variant +- Yields `vertex_info, VV>` where VV is void or invoke result +- Factory functions: `vertexlist(g)` and `vertexlist(g, vvf)` +- Uses `adjacency_list` concept (not `index_adjacency_list`) +- Constrained with `vertex_value_function` concept +- Uses `vertices()` CPO for proper iterator-based container support + +**Test Coverage** (14 test cases, 83 assertions): +- Empty graph iteration +- Single and multiple vertex iteration +- Structured bindings: `[v]` and `[v, val]` +- Various value function types (string, double, capturing, mutable) +- Deque-based graph support +- Range concepts verified (input_range, forward_range, sized_range, view) +- Iterator properties (pre/post increment, equality) +- vertex_info type verification +- Const graph access +- Weighted graph (pair edges) +- std::ranges algorithms (distance, count_if) +- **Map vertices + vector edges** (sparse vertex IDs) +- **Vector vertices + map edges** (sorted edges) +- **Map vertices + map edges** (fully sparse graph) + +**Commit Message**: +``` +[views] Phase 2.1: Implement vertexlist view + +- Yields vertex_info +- Value function receives vertex descriptor +- Supports structured bindings: [v] and [v, val] +- Tests cover iteration, value functions, const correctness +- Map-based container coverage for vertices and edges +- 83 assertions in 14 test cases +``` + +--- + +### Step 2.2: Implement incidence view ✅ COMPLETE + +**Status**: Implemented and tested (84 assertions, 15 test cases) + +**Files Created**: +- `include/graph/views/incidence.hpp` - View implementation +- `tests/views/test_incidence.cpp` - Comprehensive test suite + +**Implementation Summary**: +- `incidence_view` - No value function variant +- `incidence_view` - With value function variant +- Yields `edge_info, EV>` where EV is void or invoke result +- Factory functions: `incidence(g, u)` and `incidence(g, u, evf)` +- Uses `adjacency_list` concept +- Constrained with `edge_value_function` concept +- Iterator stores `edge_type` directly (same pattern as vertexlist) +- Uses `edges()` CPO for proper iterator-based container support + +**Test Coverage** (15 test cases, 84 assertions): +- Empty vertex (no edges) iteration +- Single and multiple edge iteration +- Structured bindings: `[e]` and `[e, val]` +- Various value function types (target_id access, edge_value access) +- Edge descriptor access (source_id, target_id) +- Weighted graph (pair edges) with edge_value +- Range concepts verified (input_range, forward_range, sized_range) +- Iterator properties (pre/post increment, equality) +- edge_info type verification +- Deque-based graph support +- All-vertices iteration pattern +- **Map vertices + vector edges** (sparse vertex IDs) +- **Vector vertices + map edges** (sorted edges) +- **Map vertices + map edges** (fully sparse graph) + +**Commit Message**: +``` +[views] Phase 2.2: Implement incidence view + +- Yields edge_info +- Edge descriptor contains source vertex descriptor +- Value function receives edge descriptor +- Supports structured bindings: [e] and [e, val] +- Map-based container coverage for vertices and edges +- 84 assertions in 15 test cases +``` + +--- + +### Step 2.3: Implement neighbors view ✅ COMPLETE + +**Status**: Implemented and tested (82 assertions, 15 test cases) + +**Files Created**: +- `include/graph/views/neighbors.hpp` - View implementation +- `tests/views/test_neighbors.cpp` - Comprehensive test suite + +**Implementation Summary**: +- `neighbors_view` - No value function variant +- `neighbors_view` - With value function variant +- Yields `neighbor_info, VV>` where VV is void or invoke result +- Factory functions: `neighbors(g, u)` and `neighbors(g, u, vvf)` +- Uses `adjacency_list` concept +- Constrained with `vertex_value_function` concept +- Uses `target()` CPO to get target vertex descriptor from edge +- Uses `edges()` CPO for proper iterator-based container support + +**Test Coverage** (15 test cases, 82 assertions): +- Empty vertex (no neighbors) iteration +- Single and multiple neighbor iteration +- Structured bindings: `[v]` and `[v, val]` +- Various value function types (string, double, capturing lambda) +- Vertex descriptor access (vertex_id) +- Weighted graph (pair edges) +- Range concepts verified (input_range, forward_range, sized_range) +- Iterator properties (pre/post increment, equality) +- neighbor_info type verification +- Deque-based graph support +- All-vertices iteration pattern (vertexlist + neighbors) +- **Map vertices + vector edges** (sparse vertex IDs) +- **Vector vertices + map edges** (sorted edges) +- **Map vertices + map edges** (fully sparse graph) + +**Commit Message**: +``` +[views] Phase 2.3: Implement neighbors view + +- Yields neighbor_info +- Provides target vertex descriptors via target() CPO +- Value function receives target vertex descriptor +- Supports structured bindings: [v] and [v, val] +- Map-based container coverage for vertices and edges +- 82 assertions in 15 test cases +``` + +--- + +### Step 2.4: Implement edgelist view for adjacency_list ✅ COMPLETED (2026-02-01) + +**Goal**: Implement edgelist view that flattens all edges from an adjacency_list, yielding `edge_info`. + +**Implementation Summary**: + +Created `include/graph/views/edgelist.hpp`: +- `edgelist_view` - Version without value function +- `edgelist_view` - Version with edge value function +- Iterator uses vertex_descriptor_view to iterate all vertices +- For each vertex, iterates its edges using `edges(g, v)` CPO +- Skips vertices with no edges automatically +- Factory functions: `edgelist(g)` and `edgelist(g, evf)` + +Created `tests/views/test_edgelist.cpp`: +- 15 test cases covering: + - Empty graph and vertices with no edges + - Single edge + - Multiple edges from single vertex + - Flattening multiple vertex edge lists + - Skipping empty vertices + - Value function types (string, double, capturing lambda) + - Range algorithms (distance, count_if, for_each, find_if) + - Container variants (vector of deques, deque of vectors) + - Iterator operations (post-increment, equality, end comparison) + - Range concepts satisfaction + - Map-based vertex containers + - Map-based edge containers (weighted edges) + - Fully sparse graphs (map vertices + map edges) +- 80 assertions in 15 test cases + +**Acceptance Criteria**: ✅ All met +- View correctly flattens all edges +- Edge descriptors contain source context (source_id/target_id work) +- Value function receives descriptor +- Structured bindings work: `for (auto [e] : edgelist(g))` and `for (auto [e, val] : edgelist(g, evf))` +- Tests pass with sanitizers +- Map-based containers supported (vov, voem, mov, moem) + +**Commit Message**: +``` +[views] Implement edgelist view for adjacency_list + +- Yields edge_info +- Flattens adjacency list structure +- Edge descriptor contains source vertex descriptor +- Value function receives edge descriptor +- Tests verify flattening and edge access +``` + +--- + +### Step 2.4.1: Implement edgelist view for edge_list ✅ COMPLETE + +**Completion Date**: 2026-02-01 +**Commit**: baeea27 "[views] Step 2.4.1: Implement edgelist view for edge_list" +**Test Results**: ✅ 26 test cases (11 new), 128 assertions, all passing + +**Goal**: Implement edgelist view that iterates over an edge_list data structure, yielding `edge_info`. + +**Files to Modify**: +- `include/graph/views/edgelist.hpp` (add edge_list overloads) + +**Implementation**: +```cpp +namespace graph::views { + +// Edge list view - wraps an edge_list range directly +template +class edge_list_edgelist_view : public std::ranges::view_interface> { + EL* el_; + [[no_unique_address]] EVF evf_; + +public: + edge_list_edgelist_view(EL& el, EVF evf) : el_(&el), evf_(std::move(evf)) {} + + class iterator { + using base_iter = std::ranges::iterator_t; + base_iter current_; + [[no_unique_address]] EVF* evf_; + + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = edge_info, + std::invoke_result_t>>; + + iterator(base_iter it, EVF* evf) + : current_(it), evf_(evf) {} + + auto operator*() const { + auto edesc = *current_; // edge_list edge descriptor + if constexpr (std::is_void_v) { + return edge_info, void>{edesc}; + } else { + return edge_info, + std::invoke_result_t>>{ + edesc, (*evf_)(edesc) + }; + } + } + + iterator& operator++() { + ++current_; + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + ++*this; + return tmp; + } + + bool operator==(const iterator& other) const { + return current_ == other.current_; + } + }; + + auto begin() { return iterator(std::ranges::begin(*el_), &evf_); } + auto end() { return iterator(std::ranges::end(*el_), &evf_); } +}; + +// Factory function for edge_list - no value function +template +auto edgelist(EL&& el) { + return edge_list_edgelist_view, void>(el, void{}); +} + +// Factory function for edge_list - with value function +template + requires edge_value_function> +auto edgelist(EL&& el, EVF&& evf) { + return edge_list_edgelist_view, std::decay_t>( + el, std::forward(evf) + ); +} + +} // namespace graph::views +``` + +**Tests to Create**: +- Extend `tests/views/test_edgelist.cpp` + - Test iteration over edge_list + - Test structured binding `[e]` and `[e, val]` + - Test edge_list edge descriptor provides source_id, target_id access + - Test with empty edge_list, single edge, multiple edges + - Test value function receives edge descriptor + - Test const edge_list behavior + - Test weighted edge_list (with edge values) + +**Acceptance Criteria**: +- View iterates over edge_list correctly +- Edge descriptors provide source/target access via CPOs +- Value function receives descriptor +- Structured bindings work +- Works with various edge_list configurations +- Tests pass with sanitizers + +**Status**: ✅ COMPLETE + +**Implementation Notes**: +- EVF signature is `EVF(EL&, edge)` not `EVF(edge)` since edge_list CPOs require edge_list reference +- Disambiguate from adjacency_list via `requires (!adj_list::adjacency_list)` +- Removed conflicting `namespace edgelist = edge_list;` alias from edge_list.hpp +- Added 11 test cases covering pairs, tuples, edge_info, weighted, empty, concepts, iterators, string VIds, algorithms, deque + +**Commit Message**: +``` +[views] Step 2.4.1: Implement edgelist view for edge_list + +- Add edge_list_edgelist_view and specializations +- Wraps edge_list ranges, yields edge_info +- Factory functions: edgelist(el) and edgelist(el, evf) +- EVF receives (EL&, edge) to support edge_list CPOs (source_id, target_id) +- Disambiguate from adjacency_list via requires (!adjacency_list) +- Remove conflicting namespace edgelist = edge_list; alias +- Add 11 new tests (Tests 16-26) +``` + +--- + +### Step 2.5: Create basic_views.hpp header ✅ COMPLETE + +**Completion Date**: 2026-02-01 +**Commit**: 8003441 "[views] Step 2.5: Add basic_views.hpp convenience header" +**Test Results**: ✅ 109 test cases (2 new), 860 assertions, all passing + +**Goal**: Create convenience header that includes all basic views. + +**Files to Create**: +- `include/graph/views/basic_views.hpp` + +**Implementation**: +```cpp +#pragma once + +#include +#include +#include +#include +``` + +**Tests to Create**: +- Update existing tests to include from basic_views.hpp +- Verify no compilation issues + +**Acceptance Criteria**: +- Header compiles cleanly +- All basic views accessible through single include +- No circular dependencies + +**Commit Message**: +``` +[views] Add basic_views.hpp convenience header + +- Includes all basic view headers +- Single include for vertexlist, incidence, neighbors, edgelist +- Tests verify compilation +``` + +--- + +## Phase 3: DFS Views + +### Step 3.1: Implement DFS infrastructure + vertices_dfs + +**Goal**: Implement DFS traversal infrastructure and vertices_dfs view. + +**Design Requirements**: +- Accept both `vertex_id_t` and `vertex_t` (vertex descriptor) as seed parameter +- Constructors accepting vertex_id delegate to vertex_descriptor constructors +- Factory functions provide overloads for both seed types +- All 4 factory function variants (no VVF, with VVF, with Alloc, with VVF+Alloc) support both seed types + +**Complexity**: +- Time: O(V + E) where V is reachable vertices and E is reachable edges + - DFS visits each reachable vertex once and traverses each reachable edge once +- Space: O(V) for the stack and visited tracker + +**Files to Create**: +- `include/graph/views/dfs.hpp` + +**Implementation**: +```cpp +namespace graph::views { + +template +class dfs_vertex_view : public std::ranges::view_interface> { + struct state_t { + using stack_entry = std::pair, edge_iterator_t>; + + std::stack> stack_; + visited_tracker, Alloc> visited_; + cancel_search cancel_ = cancel_search::continue_search; + std::size_t depth_ = 0; + std::size_t count_ = 0; + + state_t(vertex_id_t seed, std::size_t num_vertices, Alloc alloc) + : stack_(alloc), visited_(num_vertices, alloc) { + stack_.push({seed, {}}); + visited_.mark_visited(seed); + } + }; + + G* g_; + [[no_unique_address]] VVF vvf_; + std::shared_ptr state_; // See "Why shared_ptr?" below + +public: + dfs_vertex_view(G& g, vertex_id_t seed, VVF vvf, Alloc alloc) + : g_(&g), vvf_(std::move(vvf)), + state_(std::make_shared(seed, num_vertices(g), alloc)) {} + + // Why shared_ptr for state_? + // 1. Iterator copies must share state: When you copy an iterator (e.g., auto it2 = it1), + // both must refer to the same DFS traversal. Advancing it1 affects what it2 sees. + // 2. View and iterators share state: The view exposes depth(), size(), and cancel() + // accessors that reflect current traversal state modified by iterators. + // 3. Range-based for loop: The view's cancel() must be able to stop iteration in progress. + // 4. Input iterator semantics: DFS is single-pass; shared state correctly models this. + // An alternative (state by value + raw pointers) would break if the view is moved. + + cancel_search cancel() const { return state_->cancel_; } + void cancel(cancel_search c) { state_->cancel_ = c; } + std::size_t depth() const { return state_->depth_; } + std::size_t size() const { return state_->count_; } + + class iterator { + // Forward iterator implementing DFS traversal + G* g_; + std::shared_ptr state_; + [[no_unique_address]] VVF* vvf_; + bool at_end_ = false; + + void advance() { + if (state_->cancel_ == cancel_search::cancel_all || state_->stack_.empty()) { + at_end_ = true; + return; + } + + auto [vid, edge_it] = state_->stack_.top(); + state_->stack_.pop(); + + // Visit neighbors + auto [first, last] = edges(*g_, vid); + for (auto it = first; it != last; ++it) { + auto target = target_id(*g_, *it); + if (!state_->visited_.is_visited(target)) { + state_->visited_.mark_visited(target); + state_->stack_.push({target, {}}); + } + } + + ++state_->count_; + } + + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = vertex_info, + std::invoke_result_t>>; + + iterator(G* g, std::shared_ptr state, VVF* vvf, bool at_end) + : g_(g), state_(std::move(state)), vvf_(vvf), at_end_(at_end) {} + + auto operator*() const { + auto vid = state_->stack_.top().first; + auto vdesc = create_vertex_descriptor(*g_, vid); + + if constexpr (std::is_void_v) { + return vertex_info, void>{vdesc}; + } else { + return vertex_info, + std::invoke_result_t>>{ + vdesc, (*vvf_)(vdesc) + }; + } + } + + iterator& operator++() { + advance(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + ++*this; + return tmp; + } + + bool operator==(const iterator& other) const { + return at_end_ == other.at_end_; + } + }; + + auto begin() { return iterator(g_, state_, &vvf_, false); } + auto end() { return iterator(g_, state_, &vvf_, true); } +}; + +// Factory function +template> +auto vertices_dfs(G&& g, vertex_id_t seed, VVF&& vvf = {}, Alloc alloc = {}) { + if constexpr (std::is_void_v) { + return dfs_vertex_view, void, Alloc>( + g, seed, void{}, alloc + ); + } else { + return dfs_vertex_view, std::decay_t, Alloc>( + g, seed, std::forward(vvf), alloc + ); + } +} + +} // namespace graph::views +``` + +**Tests to Create**: +- `tests/views/test_dfs.cpp` + - Test DFS traversal order (pre-order) + - Test structured binding `[v]` and `[v, val]` + - Test visited tracking prevents revisiting + - Test value function receives descriptor + - Test depth() and size() accessors + - Test with various graph topologies (tree, cycle, DAG, disconnected) + - Test search_view concept satisfied + +**Acceptance Criteria**: +- DFS traversal order is correct +- Visited tracking works +- Shared state allows iterator copies +- Value function receives descriptor +- Tests pass with sanitizers + +**Commit Message**: +``` +[views] Implement DFS vertices view + +- Yields vertex_info +- Maintains visited set and DFS stack +- Supports depth(), size(), cancel() accessors +- Value function receives vertex descriptor +- Tests verify traversal order and state management +``` + +--- + +### Step 3.2: Implement edges_dfs + +**Goal**: Implement DFS edge traversal yielding `edge_info`. + +**Design Requirements**: +- Accept both `vertex_id_t` and `vertex_t` (vertex descriptor) as seed parameter +- Constructors accepting vertex_id delegate to vertex_descriptor constructors +- Factory functions provide overloads for both seed types + +**Complexity**: +- Time: O(V + E) - same as vertices_dfs, visits reachable vertices and edges +- Space: O(V) for the stack and visited tracker + +**Files to Modify**: +- `include/graph/views/dfs.hpp` (add edges_dfs implementation) + +**Implementation**: Similar structure to vertices_dfs but yields edges instead of vertices. + +**Tests to Create**: +- Extend `tests/views/test_dfs.cpp` + - Test edges_dfs traversal order + - Test structured binding `[e]` and `[e, val]` + - Test value function receives edge descriptor + - Test edge descriptor provides source/target access + +**Acceptance Criteria**: +- Edges visited in DFS order +- Edge descriptors contain source context +- Value function receives descriptor +- Tests pass + +**Commit Message**: +``` +[views] Implement DFS edges view + +- Yields edge_info +- Tree edges visited in DFS order +- Edge descriptor contains source vertex descriptor +- Value function receives edge descriptor +- Tests verify edge traversal +``` + +--- + +### Step 3.3: Test DFS cancel functionality + +**Goal**: Verify cancel_search control works correctly. + +**Tests to Create**: +- Extend `tests/views/test_dfs.cpp` + - Test cancel_branch skips subtree + - Test cancel_all stops traversal + - Test continue_search normal behavior + - Verify cancel state propagates through iterator copies + +**Acceptance Criteria**: +- Cancel functionality works as specified +- Tests verify all three cancel modes + +**Commit Message**: +``` +[views] Test DFS cancel functionality + +- Verify cancel_branch skips subtrees +- Verify cancel_all stops traversal +- Verify continue_search normal behavior +- Tests cover all cancel modes +``` + +--- + +## Phase 4: BFS Views + +### Step 4.1: Implement BFS infrastructure + vertices_bfs + +**Goal**: Implement BFS traversal infrastructure and vertices_bfs view. + +**Design Requirements**: +- Accept both `vertex_id_t` and `vertex_t` (vertex descriptor) as seed parameter +- Constructors accepting vertex_id delegate to vertex_descriptor constructors +- Factory functions provide overloads for both seed types +- All 4 factory function variants support both seed types + +**Complexity**: +- Time: O(V + E) where V is reachable vertices and E is reachable edges + - BFS visits each reachable vertex once and traverses each reachable edge once +- Space: O(V) for the queue and visited tracker + +**Files to Create**: +- `include/graph/views/bfs.hpp` + +**Implementation**: Similar to DFS but using queue instead of stack. + +**Tests to Create**: +- `tests/views/test_bfs.cpp` + - Test BFS traversal order (level-order) + - Test structured binding `[v]` and `[v, val]` + - Test visited tracking + - Test value function receives descriptor + - Test depth() and size() accessors + - Test with various graph topologies + +**Acceptance Criteria**: +- BFS traversal order is correct (level-by-level) +- Visited tracking works +- Value function receives descriptor +- Tests pass with sanitizers + +**Commit Message**: +``` +[views] Implement BFS vertices view + +- Yields vertex_info +- Maintains visited set and BFS queue +- Supports depth(), size(), cancel() accessors +- Value function receives vertex descriptor +- Tests verify level-order traversal +``` + +--- + +### Step 4.2: Implement edges_bfs + +**Goal**: Implement BFS edge traversal. + +**Design Requirements**: +- Accept both `vertex_id_t` and `vertex_t` (vertex descriptor) as seed parameter +- Constructors accepting vertex_id delegate to vertex_descriptor constructors +- Factory functions provide overloads for both seed types + +**Complexity**: +- Time: O(V + E) - same as vertices_bfs, visits reachable vertices and edges +- Space: O(V) for the queue and visited tracker + +**Files to Modify**: +- `include/graph/views/bfs.hpp` + +**Tests to Create**: +- Extend `tests/views/test_bfs.cpp` + +**Acceptance Criteria**: +- BFS edge traversal works correctly +- Tests pass + +**Commit Message**: +``` +[views] Implement BFS edges view + +- Yields edge_info +- Edges visited in BFS order +- Tests verify edge traversal +``` + +--- + +### Step 4.3: Test BFS depth/size accessors + +**Goal**: Verify depth() and size() tracking is accurate. + +**Tests to Create**: +- Extend `tests/views/test_bfs.cpp` + - Verify depth increases by level + - Verify size counts visited vertices + - Test on various graph structures + +**Acceptance Criteria**: +- Depth and size tracking is accurate +- Tests pass + +**Commit Message**: +``` +[views] Test BFS depth/size accessors + +- Verify depth tracks BFS levels correctly +- Verify size counts visited vertices +- Tests cover various graph topologies +``` + +--- + +## Phase 5: Topological Sort Views + +**Design Rationale - Why Not Seed-Based?** + +Unlike DFS/BFS, topological sort is intentionally designed as a **whole-graph operation** without seed vertices. This decision was made for several reasons: + +1. **Semantic Correctness**: Topological sort defines a global ordering constraint - all vertices must be ordered such that for every edge (u,v), u comes before v. A "partial topological sort from seed" doesn't satisfy the classical definition and is really just "DFS post-order in reverse." + +2. **Use Case Alignment**: Primary use cases require the full graph: + - Build systems (make, CMake, ninja) - all tasks and dependencies + - Course prerequisites - entire curriculum ordering + - Package managers - all packages with dependencies + - Task scheduling - complete task set with constraints + +3. **API Simplicity**: Avoiding seed-based variants keeps the API focused with 4 factory functions instead of 20+ overloads (single seed × vertex_id/descriptor × 4 variants + range seeds × 4 variants). + +4. **Conceptual Clarity**: Clear semantics - processes entire graph in topological order. For seed-based exploration, use `vertices_dfs` and reverse the post-order. + +5. **Implementation Trade-off**: Favors the 90% case (whole-graph ordering) over the 10% case (partial ordering from seed), which can be achieved through composition with other views. + +### Step 5.1: Implement topological sort + vertices_topological_sort ✅ (2026-02-01) + +**Goal**: Implement topological sort algorithm and vertices view. + +**Design Requirements**: +- Process all vertices in graph (not seed-based) +- Use reverse DFS post-order algorithm +- Forward iterator (vector-based) +- Factory functions provide value function and allocator overloads + +**Complexity**: +- Time: O(V + E) where V is vertices and E is edges + - DFS visits each vertex once and traverses each edge once +- Space: O(V) for post-order vector and visited tracker + +**Files Created**: +- `include/graph/views/topological_sort.hpp` +- `tests/views/test_topological_sort.cpp` + +**Implementation**: Reverse DFS post-order - visit all children before adding vertex to result. + +**Tests Created**: +- 13 test cases with 66 assertions +- Topological order verification (all edges point forward) +- Structured bindings `[v]` and `[v, val]` +- Value function receives descriptor +- Various DAG structures (diamond, linear chain, complex, wide, disconnected) +- Size accessor +- Empty graph + +**Acceptance Criteria**: ✅ +- Topological order is correct (all edges point forward) +- Value function receives descriptor +- Forward iterator (multi-pass) +- All tests pass +- 200 total view tests, 2355 assertions + +**Commit Message**: +``` +[views] Implement topological sort vertices view + +- Yields vertex_info +- Uses reverse DFS post-order algorithm +- Produces valid topological ordering +- Processes entire graph (not seed-based) +- Forward iterator (vector-based iteration) +- Tests verify ordering on various DAGs +``` + +--- + +### Step 5.2: Implement edges_topological_sort ✅ + +**Goal**: Implement topological edge traversal. + +**Files Modified**: +- `include/graph/views/topological_sort.hpp` +- `tests/views/test_topological_sort.cpp` + +**Implementation Details**: +- Two edges view classes (void and EVF variants) +- Forward iterator, nested vertex/edge iteration +- Vertices ordered topologically, edges from each vertex in order +- 4 factory functions with complexity documentation +- 8 test cases, all passing + +**Test Coverage** (21 tests total, 99 assertions): +- Simple, diamond, and complex DAG edge traversal +- Structured bindings [e] and [e, val] +- Value function receives edge descriptor +- Verification that edges follow topological ordering +- Disconnected components +- Empty graph and no-edge graph + +**Complexity**: +- Time: O(V + E) - DFS traversal plus edge iteration +- Space: O(V) - stores all vertices in result vector + +**Commit**: Done + +``` +[views] Implement topological sort edges view + +- edges_topological_sort_view - no value function +- edges_topological_sort_view - with value function +- Edges iterate in topological order (by source vertex) +- 8 test cases verifying edge order and completeness +``` + +--- + +### Step 5.3: Test cycle detection + +**Goal**: Verify behavior on graphs with cycles. + +**Tests to Create**: +- Extend `tests/views/test_topological_sort.cpp` + - Test cycle detection throws or returns empty + - Test various cycle patterns +--- + +### Step 5.3: Test cycle detection ✅ + +**Goal**: Verify behavior on graphs with cycles. + +**Files Modified**: +- `tests/views/test_topological_sort.cpp` + +**Implementation Details**: +- Added 7 comprehensive cycle detection tests +- Documented current behavior: no explicit cycle detection +- Tests verify ordering produced on cyclic graphs +- Tests demonstrate backward edges in invalid orderings + +**Test Coverage** (28 tests total, 120 assertions): +- Self-loop cycle (vertex points to itself) +- Simple cycle (0→1→2→0) +- Cycle with acyclic tail (DAG leading into cycle) +- Multiple disjoint cycles +- Edge iteration on cyclic graphs +- Documentation test explaining behavior and rationale + +**Current Behavior Documentation**: +- Topological sort does NOT detect or reject cycles +- On cyclic graphs, produces an ordering containing all vertices +- The ordering is NOT a valid topological sort (some edges point backward) +- DFS-based implementation prioritizes performance over validation +- Users responsible for ensuring input is a DAG if valid ordering needed + +**Rationale**: +- Cycle detection requires additional state tracking (e.g., "on stack" marks) +- Adds overhead to the common (acyclic) case +- Behavior on cycles is well-defined but produces invalid orderings +- Future enhancement could add optional cycle detection with flag + +**Commit**: Done + +``` +[views] Test topological sort cycle behavior + +- 7 tests covering various cycle patterns +- Documents behavior: produces ordering but invalid for cycles +- Tests verify backward edges exist on cyclic graphs +- Self-loop, simple cycle, cycle with tail, multiple cycles +- Edges iteration on cyclic graphs +``` + +--- + +### Step 5.4: Add optional cycle detection with tl::expected ✅ + +**Goal**: Add safe variants of topological sort functions that detect cycles. + +**Files Modified**: +- `CMakeLists.txt` - Added tl::expected dependency via CPMAddPackage +- `include/graph/views/topological_sort.hpp` +- `tests/views/test_topological_sort.cpp` + +**Implementation Details**: +- Integrated tl::expected library (C++23 std::expected polyfill for C++20) +- Modified `topo_state` to support optional cycle detection with recursion stack tracking +- Added 8 `_safe` factory functions: `vertices_topological_sort_safe` and `edges_topological_sort_safe` +- Returns `tl::expected>` - view on success, cycle vertex on failure +- Cycle detection uses DFS with recursion stack (back edge detection) +- Zero overhead when using regular (non-safe) functions + +**API Design**: +```cpp +// Returns expected with view or vertex that closes cycle +auto result = vertices_topological_sort_safe(g); +if (result) { + for (auto [v] : *result) { ... } +} else { + auto cycle_v = result.error(); + // Handle cycle detected at cycle_v +} + +// With value function +auto result = vertices_topological_sort_safe(g, value_fn); + +// Edges variant +auto result = edges_topological_sort_safe(g); +``` + +**Test Coverage** (39 tests total, 154 assertions): +- 11 new safe variant tests (34 assertions) +- Valid DAG processing with safe functions +- Simple cycle, self-loop, cycle with tail detection +- Value function on DAG and cyclic graphs +- Diamond DAG (no cycle) +- Edge variants with cycle detection +- Usage pattern documentation + +**Complexity** (when cycle detection enabled): +- Time: O(V + E) with ~20-30% overhead for stack tracking +- Space: O(2V) - original O(V) plus O(V) for recursion stack +- Early exit on cycle detection (may be faster than full traversal) + +**Benefits**: +- Actionable error information (returns specific cycle vertex) +- Zero-cost abstraction (no heap allocation, no exceptions) +- Composable with monadic operations (.and_then(), .or_else()) +- Self-documenting API (return type shows success/failure cases) +- Smooth migration path to C++23 std::expected + +**Commit**: Done + +``` +[views] Add safe topological sort with cycle detection + +- Integrated tl::expected via CPMAddPackage (C++23 std::expected polyfill) +- Added vertices_topological_sort_safe() and edges_topological_sort_safe() +- Returns tl::expected> - view or cycle vertex +- DFS with recursion stack tracking for back edge detection +- 11 tests verifying cycle detection and valid DAG processing +- Zero overhead when using non-safe variants +- O(V+E) time with 20-30% overhead, O(2V) space when enabled +``` + +--- + +## Phase 6: Range Adaptors + +### Step 6.1: Implement range adaptor closures for basic views + +**Goal**: Implement pipe syntax support for basic views. + +**Files to Create**: +- `include/graph/views/adaptors.hpp` + +**Implementation**: +```cpp +namespace graph::views { + +// Adaptor for vertexlist +template +struct vertexlist_adaptor { + [[no_unique_address]] VVF vvf; + + template + friend auto operator|(G&& g, vertexlist_adaptor adaptor) { + if constexpr (std::is_void_v) { + return vertexlist(std::forward(g)); + } else { + return vertexlist(std::forward(g), std::move(adaptor.vvf)); + } + } +}; + +inline constexpr auto vertexlist_adaptor_fn = [](VVF&& vvf = {}) { + return vertexlist_adaptor>{std::forward(vvf)}; +}; + +// Similar for incidence, neighbors, edgelist... + +} // namespace graph::views + +namespace graph::views::inline adaptors { + inline constexpr auto vertexlist = vertexlist_adaptor_fn; + // ... other adaptors +} +``` + +**Tests to Create**: +- `tests/views/test_adaptors.cpp` + - Test `g | vertexlist()` syntax + - Test `g | vertexlist(vvf)` with value function + - Test `g | incidence(uid)` syntax + - Test chaining: `g | vertexlist() | std::views::take(5)` + +**Acceptance Criteria**: +- Pipe syntax compiles and works correctly +- All basic views support pipe operator +- Chaining with standard views works +- Tests pass + +**Commit Message**: +``` +[views] Implement range adaptor closures for basic views + +- Support g | vertexlist() pipe syntax +- Support g | incidence(uid) pipe syntax +- Support g | neighbors(uid) and g | edgelist() +- Enable chaining with standard range adaptors +- Tests verify pipe syntax and chaining +``` + +--- + +### Step 6.2: Implement range adaptor closures for search views ✅ COMPLETE + +**Completion Date**: 2026-02-01 +**Status**: ✅ COMPLETE + +**Goal**: Implement pipe syntax for search views and topological sort views. + +**Files Modified**: +- `include/graph/views/adaptors.hpp` +- `tests/views/test_adaptors.cpp` + +**Implementation Summary**: +- Added range adaptor closures for DFS views: `vertices_dfs_adaptor_closure`, `edges_dfs_adaptor_closure` +- Added range adaptor closures for BFS views: `vertices_bfs_adaptor_closure`, `edges_bfs_adaptor_closure` +- Added range adaptor closures for topological sort views: `vertices_topological_sort_adaptor_closure`, `edges_topological_sort_adaptor_closure` +- Each adaptor supports: + * Pipe syntax: `g | vertices_dfs(seed)`, `g | vertices_topological_sort()` + * With value function: `g | vertices_bfs(seed, vvf)`, `g | vertices_topological_sort(vvf)` + * Direct call: `vertices_dfs(g, seed)`, `vertices_topological_sort(g)` + * Optional allocator parameter: `g | vertices_dfs(seed, vvf, alloc)` +- Added adaptor function objects: `vertices_dfs`, `edges_dfs`, `vertices_bfs`, `edges_bfs`, `vertices_topological_sort`, `edges_topological_sort` + +**Test Results**: ✅ 39 test cases, 79 assertions, all passing + +**Tests Added**: +- Basic pipe syntax for all search views (DFS, BFS) +- Basic pipe syntax for topological sort views (no seed required) +- With value function for all views +- Chaining with `std::views::transform` and `std::views::filter` +- Direct call compatibility verification +- Topological order verification for DAGs + +**Key Implementation Details**: +- DFS/BFS views require a seed parameter (vertex ID or vertex descriptor) +- Topological sort views process entire graph (no seed parameter) +- Adaptors store seed (if applicable), optional value function, and optional allocator +- Pattern matches basic view adaptors but includes seed in closure for search views +- All views chain properly with standard range adaptors + +**Acceptance Criteria**: ✅ All met +- Pipe syntax works for all search views +- Pipe syntax works for topological sort views +- Value functions work correctly +- Chaining with `std::views` adaptors works +- Tests pass with all view combinations + +**Commit Message**: +``` +[views] Step 6.2: Implement range adaptor closures for search views + +- Add vertices_dfs, edges_dfs, vertices_bfs, edges_bfs adaptors +- Add vertices_topological_sort, edges_topological_sort adaptors +- Support g | vertices_dfs(seed) pipe syntax +- Support g | vertices_topological_sort() pipe syntax (no seed) +- Support g | vertices_bfs(seed, vvf) with value functions +- Support optional allocator parameter +- Add comprehensive tests for search view pipe syntax +- Tests verify chaining with std::views adaptors +- Tests verify topological order correctness +- All 39 adaptor tests passing (79 assertions) +``` + +--- + +### Step 6.3: Test pipe syntax and chaining ✅ COMPLETE + +**Completion Date**: 2026-02-01 +**Status**: ✅ COMPLETE + +**Goal**: Comprehensive testing of range adaptor functionality. + +**Files Modified**: +- `tests/views/test_adaptors.cpp` + +**Implementation Summary**: +Added 12 comprehensive chaining and integration tests covering: +- Multiple transform chains (3+ transforms in sequence) +- Filter + transform combinations +- Transform + filter + transform complex chains +- Integration with std::views::take +- Integration with std::views::drop +- Chaining with incidence views +- Chaining with neighbors views +- Const correctness tests (const graphs with pipes) +- Const correctness with chaining +- Mixing different view types in chains +- Complex DFS chaining with multiple filters +- Edgelist chaining with transforms + +**Test Results**: ✅ 51 test cases, 101 assertions, all passing + +**Tests Added** (12 new test cases): +1. Complex chaining - multiple transforms +2. Complex chaining - filter and transform +3. Complex chaining - transform, filter, transform +4. Chaining with std::views::take +5. Chaining with std::views::drop +6. Chaining incidence with transforms +7. Chaining neighbors with filter +8. Const correctness - const graph with pipe +9. Const correctness - const graph with chaining +10. Mixing different view types in chains +11. Search views - complex chaining with multiple filters +12. Edgelist chaining with reverse + +**Key Test Coverage**: +- All basic views work with standard range adaptors +- Complex chains (3+ operations) work correctly +- Const graphs work with all adaptors +- Multiple filters and transforms can be chained +- Views can be mixed in nested loops +- Integration with std::views library is seamless + +**Acceptance Criteria**: ✅ All met +- Complex chains work correctly +- Integration with standard range adaptors verified +- Const correctness confirmed +- All view types tested in chains + +**Commit Message**: +``` +[views] Step 6.3: Comprehensive pipe syntax and chaining tests + +- Add 12 new comprehensive chaining tests +- Test multiple transforms in sequence +- Test filter + transform combinations +- Test integration with std::views::take and drop +- Test const correctness with pipes and chains +- Test mixing different view types in chains +- Test complex DFS chaining with multiple operations +- All 51 adaptor tests passing (101 assertions) +``` + +--- + +## Phase 7: Integration & Polish + +### Step 7.1: Create unified views.hpp header + +**Goal**: Create master header for all views. + +**Files to Create**: +- `include/graph/views.hpp` + +**Implementation**: +```cpp +#pragma once + +#include +#include +#include +#include +#include +``` + +**Tests to Create**: +- Verify single include works +- Verify no compilation issues + +**Acceptance Criteria**: +- Master header compiles cleanly +- All views accessible +- No circular dependencies + +**Commit Message**: +``` +[views] Add unified views.hpp master header + +- Includes all view headers +- Single include for complete views API +- Tests verify compilation +``` + +--- + +### Step 7.2: Update graph.hpp to include views + +**Goal**: Make views available through main graph header. + +**Files to Modify**: +- `include/graph/graph.hpp` + +**Implementation**: +```cpp +#include +``` + +**Tests to Create**: +- Verify graph.hpp includes views +- Test that including graph.hpp gives access to all views + +**Acceptance Criteria**: +- Views available through graph.hpp +- No compilation issues +- Tests pass + +**Commit Message**: +``` +[views] Include views in main graph.hpp header + +- Add #include to graph.hpp +- Views now available through main header +- Tests verify availability +``` + +--- + +### Step 7.3: Write documentation + +**Goal**: Create comprehensive documentation for views. + +**Files to Create**: +- `docs/views.md` + +**Content**: +- Overview of views +- Basic views documentation with examples +- Search views documentation with examples +- Range adaptor syntax examples +- Value function usage patterns +- Performance considerations +- Best practices + +**Acceptance Criteria**: +- Documentation is comprehensive and clear +- All views documented with examples +- Code examples compile and run + +**Commit Message**: +``` +[views] Add comprehensive views documentation + +- Document all basic and search views +- Provide usage examples for each view +- Document range adaptor syntax +- Add value function patterns +- Include performance notes +``` + +--- + +### Step 7.4: Performance benchmarks ✅ COMPLETE + +**Goal**: Create benchmarks to measure view performance. + +**Files Created**: +- `benchmark/benchmark_views.cpp` (520 lines) +- Updated `benchmark/CMakeLists.txt` +- Fixed `benchmark/benchmark_vertex_access.cpp` (namespace issue) + +**Implementation Status**: +- ✅ 25 benchmarks created covering all views +- ✅ Basic views: vertexlist, incidence, neighbors, edgelist +- ✅ Search views: DFS, BFS, topological sort (vertices and edges) +- ✅ Comparison benchmarks: view vs manual iteration +- ✅ Chaining benchmarks: filter, transform, take +- ✅ Graph type benchmarks: path graphs, complete graphs +- ✅ Complexity analysis with ->Complexity() +- ✅ All benchmarks compile and run successfully + +**Sample Results**: +``` +BM_Vertexlist_Iteration_BigO 0.08 N +BM_TopoSort_Vertices_BigO 7.84 N (linear) +BM_TopoSort_Edges_BigO 10.00 N (linear) +``` + +**Performance Characteristics**: +- Vertexlist: O(n) - negligible overhead +- Search views: O(V + E) - expected complexity maintained +- Chaining: Minimal overhead with standard library views +- Complete graphs: Dense graph handling verified + +**Commit**: Pending (include with Step 7.4) + +--- + +### Step 7.5: Edge case testing ✅ COMPLETE + +**Completion Date**: 2026-02-01 +**Test Results**: ✅ 32 test cases, 3119 assertions, all passing + +**Goal**: Comprehensive edge case coverage. + +**Files Created**: +- `tests/views/test_edge_cases.cpp` (647 lines) +- Updated `tests/views/CMakeLists.txt` + +**Test Coverage** (32 test cases): +- **Empty Graphs**: Empty graph iteration with vertexlist and edgelist +- **Single Vertex**: Single vertex with no edges, and with self-loop +- **Disconnected Graphs**: DFS/BFS reach only one component, topological sort includes all +- **Self-Loops**: Multiple vertices with self-loops, incidence, neighbors, edgelist +- **Parallel Edges**: Multiple edges between same vertices +- **Const Graphs**: All basic views work with const graphs +- **Alternative Containers**: Deque-based graphs for basic views +- **Sparse Graphs**: Non-contiguous vertex IDs +- **Value Functions**: Capturing lambdas, mutable lambdas, structured bindings +- **Exception Safety**: Value functions that throw exceptions +- **Large Graphs**: Stress tests with 1000-10000 vertices +- **Iterator Stability**: View outlives iterators, view copy independence +- **Empty Ranges**: Graphs with vertices but no edges + +**Edge Cases Covered**: +✅ Empty graphs (no vertices) +✅ Single vertex graphs (with/without edges) +✅ Disconnected graphs (multiple components) +✅ Self-loops (vertices pointing to themselves) +✅ Parallel edges (multiple edges between same pair) +✅ Const correctness (const graphs) +✅ Alternative containers (deque) +✅ Sparse vertex IDs (non-contiguous) +✅ Value function variants (capturing, mutable) +✅ Exception safety (throwing value functions) +✅ Large graphs (stress tests up to 10K vertices) +✅ Iterator stability and view copies + +**Acceptance Criteria**: ✅ All met +- All edge cases handled correctly +- No crashes or undefined behavior +- 3119 assertions passing across 32 test cases +- Tests compile with warning flags + +**Commit**: Pending + +--- + +**Phase 7 Status**: ✅ COMPLETE (2026-02-01) +All 5 steps completed successfully. + +--- + +## Final Integration + +### Merge to main + +**Prerequisites**: +- All steps completed +- All tests passing +- Documentation complete +- Code reviewed +- Benchmarks run and documented + +**Process**: +1. Rebase feature branch on latest main +2. Run full test suite +3. Run sanitizers +4. Run benchmarks +5. Create pull request +6. Address review comments +7. Merge to main + +**Final Commit Message**: +``` +[views] Complete graph views implementation + +This PR implements graph views as described in D3129: + +**Basic Views**: +- vertexlist: iterate over all vertices +- incidence: iterate over outgoing edges +- neighbors: iterate over adjacent vertices +- edgelist: iterate over all edges (flattened) + +**Search Views**: +- vertices_dfs/edges_dfs (source accessible via edge descriptor) +- vertices_bfs/edges_bfs (source accessible via edge descriptor) +- vertices_topological_sort/edges_topological_sort (source accessible via edge descriptor) + +**Features**: +- Descriptor-based design (value functions receive descriptors) +- Info structs with optional members via void template parameters +- Range adaptor pipe syntax (g | view(...)) +- Lazy evaluation with zero-copy where possible +- Comprehensive test coverage +- Full documentation +- Performance benchmarks + +Closes # +``` + +--- + +## Notes for Agent Execution + +### General Guidelines +- **Incremental commits**: Commit after each step completion +- **Test-driven**: Write tests first or alongside implementation +- **Code quality**: Follow project style guidelines +- **Error handling**: Check for errors, use concepts for constraints +- **Documentation**: Add inline comments for complex logic +- **Sanitizers**: Run with ASAN/UBSAN/TSAN to catch issues + +### Testing Strategy +- Use Catch2 test framework +- Test with both vector and deque based graphs +- Include const correctness tests +- Test with empty graphs +- Test boundary conditions +- Use structured bindings in tests to verify info struct layout + +### Performance Considerations +- Use `[[no_unique_address]]` for empty base optimization +- Avoid unnecessary copies (use references where appropriate) +- Consider lazy evaluation for value functions +- Share state between iterators using `std::shared_ptr` for search views: + - Enables iterator copies to share traversal state (required for input iterators) + - Allows view's cancel()/depth()/size() to reflect iterator progress + - Survives view moves (raw pointers would dangle) + - Slight overhead vs. raw pointers, but correctness requires it + +### Common Pitfalls to Avoid +- Don't forget to handle empty graphs +- Don't forget const correctness +- Don't assume random access (some graphs use deque) +- Don't hardcode vertex/edge types +- Test with both void and non-void value function cases + +--- + +**End of Implementation Plan** diff --git a/agents/view_strategy.md b/agents/view_strategy.md new file mode 100644 index 0000000..db0ddd1 --- /dev/null +++ b/agents/view_strategy.md @@ -0,0 +1,1578 @@ +# Graph Views Implementation Strategy + +This document describes the strategy for implementing graph views as described in D3129. + +**Status**: Phase 0 Complete (2026-01-31) +**Next Phase**: Phase 1 (Foundation) + +## 1. Overview + +Views provide lazy, range-based access to graph elements during traversal. They enable algorithms +to iterate over vertices, edges, and neighbors using standard range-based for loops with structured +bindings. Views do not own data—they reference the underlying graph and synthesize info structs +on iteration. + +### 1.1 Design Principles + +1. **Lazy Evaluation**: Views compute elements on-demand during iteration +2. **Zero-Copy**: Return references where possible; IDs are copied (cheap) +3. **Structured Bindings**: All views yield info structs supporting `auto&& [a, b, ...]` +4. **Value Functions**: Optional callable parameters to extract custom values +5. **Const-Correct**: Views from const graphs yield const references + +### 1.2 View Categories + +| Category | Views | Description | +|----------|-------|-------------| +| Basic | `vertexlist`, `incidence`, `neighbors`, `edgelist` | Direct graph element access | +| Search | `vertices_dfs`, `edges_dfs`, `sourced_edges_dfs` | Depth-first traversal | +| Search | `vertices_bfs`, `edges_bfs`, `sourced_edges_bfs` | Breadth-first traversal | +| Topological | `vertices_topological_sort`, `edges_topological_sort`, `sourced_edges_topological_sort` | DAG linearization | + +--- + +## 2. Info Structs + +Info structs are the value types yielded by view iterators. The existing implementation in +`graph_info.hpp` provides a solid foundation. The current design uses template specializations +with `void` to conditionally include/exclude members. + +### 2.1 Current Implementation (graph_info.hpp) + +The codebase already has: + +```cpp +template +struct vertex_info { source_id_type id; vertex_type vertex; value_type value; }; +// + specializations for void combinations + +template +struct edge_info { source_id_type source_id; target_id_type target_id; edge_type edge; value_type value; }; +// + 8 specializations for Sourced × E × EV combinations + +template +struct neighbor_info { source_id_type source_id; target_id_type target_id; vertex_type target; value_type value; }; +// + 8 specializations for Sourced × V × VV combinations +``` + +### 2.2 Required Enhancements + +**Task 2.2.1**: Add search-specific info struct extensions for DFS/BFS/topological views: + +```cpp +// For search views that need depth information +template +struct search_vertex_info : vertex_info { + std::size_t depth; // Distance from search root +}; + +template +struct search_edge_info : edge_info { + std::size_t depth; // Distance from search root +}; + +template +struct search_neighbor_info : neighbor_info { + std::size_t depth; // Distance from search root +}; +``` + +**Design Decision**: Consider whether depth should be: +- A member of the info struct (current proposal) +- Accessed via `depth(info)` CPO (more flexible, enables lazy computation) +- Stored only in the view state and accessed via `view.depth()` (D3129 approach) + +**Recommendation**: Follow D3129—depth accessible via the view, not the info struct. + +--- + +## 3. Basic Views + +### 3.1 vertexlist View + +**Purpose**: Iterate over all vertices in the graph. + +**Signature**: +```cpp +template +auto vertexlist(G&& g, VVF&& vvf = {}) + -> /* range of vertex_info, invoke_result_t>> */ +``` + +**Parameters**: +- `g`: The graph (lvalue or rvalue reference) +- `vvf`: Optional vertex value function `VV vvf(vertex_descriptor_t)` + +**Returns**: Range yielding `vertex_info` where: +- `VId = void` (descriptor contains ID) +- `V = vertex_descriptor_t` +- `VV = invoke_result_t>` (or `void` if no vvf) + +**Implementation Strategy**: +```cpp +template +class vertexlist_view : public std::ranges::view_interface> { + G* g_; + VVF vvf_; // Stored if provided + + class iterator { + G* g_; + vertex_id_t current_; + VVF* vvf_; + + auto operator*() const -> vertex_info, ...> { + auto vdesc = vertex_descriptor<...>(current_); + if constexpr (std::is_void_v) { + return {vdesc}; // No value + } else { + return {vdesc, (*vvf_)(vdesc)}; // With value + } + } + }; +}; +``` + +**File Location**: `include/graph/views/vertexlist.hpp` + +--- + +### 3.2 incidence View + +**Purpose**: Iterate over outgoing edges from a vertex. + +**Signature**: +```cpp +template +auto incidence(G&& g, vertex_id_t uid, EVF&& evf = {}) + -> /* range of edge_info, invoke_result_t>> */ +``` + +**Parameters**: +- `g`: The graph +- `uid`: Source vertex ID +- `evf`: Optional edge value function `EV evf(edge_descriptor_t)` + +**Returns**: Range yielding `edge_info` where: +- `VId = void` (descriptor contains source/target IDs) +- `Sourced = true` (edge descriptor contains source vertex descriptor) +- `E = edge_descriptor_t` +- `EV = invoke_result_t>` (or `void` if no evf) + +**Implementation Notes**: +- Wraps `edges(g, u)` CPO +- Creates edge descriptors with source vertex context (uid parameter) +- Edge descriptor provides access to source_id, target_id, and edge data +- Value function receives edge descriptor for maximum flexibility + +**File Location**: `include/graph/views/incidence.hpp` + +--- + +### 3.3 neighbors View + +**Purpose**: Iterate over adjacent vertices (neighbors) from a vertex. + +**Signature**: +```cpp +template +auto neighbors(G&& g, vertex_id_t uid, VVF&& vvf = {}) + -> /* range of neighbor_info, invoke_result_t>> */ +``` + +**Parameters**: +- `g`: The graph +- `uid`: Source vertex ID +- `vvf`: Optional vertex value function applied to target vertex descriptors `VV vvf(vertex_descriptor_t)` + +**Returns**: Range yielding `neighbor_info` where: +- `VId = void` (descriptor contains target ID) +- `Sourced = false` (focus on target vertex, not edge source) +- `V = vertex_descriptor_t` (target vertex descriptor) +- `VV = invoke_result_t>` (or `void` if no vvf) + +**Implementation Notes**: +- Iterates over `edges(g, u)` internally to navigate adjacency +- Yields neighbor_info containing target vertex descriptor +- Value function receives target vertex descriptor (not source) + +**File Location**: `include/graph/views/neighbors.hpp` + +--- + +### 3.4 edgelist View + +**Purpose**: Iterate over all edges in the graph (flat edge list). + +**Signature**: +```cpp +template +auto edgelist(G&& g, EVF&& evf = {}) + -> /* range of edge_info, invoke_result_t>> */ +``` + +**Parameters**: +- `g`: The graph +- `evf`: Optional edge value function `EV evf(edge_descriptor_t)` + +**Returns**: Range yielding `edge_info` where: +- `VId = void` (descriptor contains IDs) +- `Sourced = true` (always sourced) +- `E = edge_descriptor_t` +- `EV = invoke_result_t>` (or `void` if no evf) + +**Implementation Notes**: +- Flattens the two-level `for u in vertices(g): for e in edges(g, u)` iteration +- Uses `std::views::join` or custom join iterator +- Each edge includes source_id from the outer loop + +**File Location**: `include/graph/views/edgelist.hpp` + +--- + +## 4. Search Views + +Search views maintain internal state (visited set, stack/queue) and provide traversal control. + +### 4.1 Common Search Infrastructure + +**Search Control (from D3129)**: +```cpp +enum class cancel_search { + continue_search, // Continue normal traversal + cancel_branch, // Skip subtree, continue with siblings + cancel_all // Stop entire search +}; + +// Accessible on search view iterators/ranges: +cancel_search cancel(search_view_type& view); // Get/set cancel state +std::size_t depth(const search_view_type& view); // Current depth from root +std::size_t size(const search_view_type& view); // Elements processed so far +``` + +**Visited Tracking**: +```cpp +template> +class visited_tracker { + std::vector visited_; +public: + bool is_visited(VId id) const; + void mark_visited(VId id); + void reset(); +}; +``` + +**Allocator Requirements**: +- Search views accept an `Alloc` template parameter for custom allocators +- The allocator type must satisfy the standard allocator requirements +- For DFS/BFS: allocates storage for visited tracking (`std::vector`) and stack/queue entries +- Default: `std::allocator` for visited vector; stack/queue use standard allocators for their entry types +- Custom allocators useful for memory pools, PMR, or embedded systems + +**File Location**: `include/graph/views/search_base.hpp` + +--- + +### 4.2 DFS Views + +**Purpose**: Depth-first traversal from a source vertex. + +#### 4.2.1 vertices_dfs + +```cpp +template> +auto vertices_dfs(G&& g, vertex_id_t seed, VVF&& vvf = {}, Alloc alloc = {}) + -> /* dfs_view yielding vertex_info */ + +// Vertex descriptor overload +template> +auto vertices_dfs(G&& g, vertex_t seed_vertex, VVF&& vvf = {}, Alloc alloc = {}) + -> /* dfs_view yielding vertex_info */ +``` + +**Yields**: `vertex_info` in DFS order (VId=void; IDs accessible via descriptor) + +#### 4.2.2 edges_dfs + +```cpp +template> +auto edges_dfs(G&& g, vertex_id_t seed, EVF&& evf = {}, Alloc alloc = {}) + -> /* dfs_view yielding edge_info */ + +// Vertex descriptor overload +template> +auto edges_dfs(G&& g, vertex_t seed_vertex, EVF&& evf = {}, Alloc alloc = {}) + -> /* dfs_view yielding edge_info */ +``` + +**Yields**: `edge_info` (non-sourced) in DFS order (VId=void; IDs accessible via descriptor) + +#### 4.2.3 sourced_edges_dfs + +```cpp +template> +auto sourced_edges_dfs(G&& g, vertex_id_t seed, EVF&& evf = {}, Alloc alloc = {}) + -> /* dfs_view yielding sourced edge_info */ +``` + +**Yields**: `edge_info` in DFS order (VId=void; source/target IDs accessible via descriptor) + +**DFS Implementation Strategy**: +```cpp +template +class dfs_view : public std::ranges::view_interface> { + G* g_; + VF value_fn_; + + struct state_t { + using stack_entry = std::pair, edge_iterator_t>; + std::stack> stack_; + std::vector visited_; + std::size_t depth_ = 0; + std::size_t count_ = 0; + cancel_search cancel_ = cancel_search::continue_search; + }; + std::shared_ptr state_; // Shared for iterator copies + + class iterator { + // Forward iterator over DFS traversal + }; +}; +``` + +**File Location**: `include/graph/views/dfs.hpp` + +--- + +### 4.3 BFS Views + +**Purpose**: Breadth-first traversal from a source vertex. + +#### 4.3.1 vertices_bfs + +```cpp +template> +auto vertices_bfs(G&& g, vertex_id_t seed, VVF&& vvf = {}, Alloc alloc = {}) + -> /* bfs_view yielding vertex_info */ + +// Vertex descriptor overload +template> +auto vertices_bfs(G&& g, vertex_t seed_vertex, VVF&& vvf = {}, Alloc alloc = {}) + -> /* bfs_view yielding vertex_info */ +``` + +**Yields**: `vertex_info` in BFS order (VId=void; IDs accessible via descriptor) + +#### 4.3.2 edges_bfs + +```cpp +template> +auto edges_bfs(G&& g, vertex_id_t seed, EVF&& evf = {}, Alloc alloc = {}) + -> /* bfs_view yielding edge_info */ + +// Vertex descriptor overload +template> +auto edges_bfs(G&& g, vertex_t seed_vertex, EVF&& evf = {}, Alloc alloc = {}) + -> /* bfs_view yielding edge_info */ +``` + +**Yields**: `edge_info` (non-sourced) in BFS order (VId=void; IDs accessible via descriptor) + +#### 4.3.3 sourced_edges_bfs + +```cpp +template> +auto sourced_edges_bfs(G&& g, vertex_id_t seed, EVF&& evf = {}, Alloc alloc = {}) + -> /* bfs_view yielding sourced edge_info */ +``` + +**Yields**: `edge_info` (sourced) in BFS order (VId=void; IDs accessible via descriptor) + +**BFS Implementation**: Same as DFS but uses `std::queue` instead of `std::stack`. + +**File Location**: `include/graph/views/bfs.hpp` + +--- + +### 4.4 Topological Sort Views + +**Purpose**: Linear ordering of vertices in a DAG. + +#### 4.4.1 vertices_topological_sort + +```cpp +template> +auto vertices_topological_sort(G&& g, VVF&& vvf = {}, Alloc alloc = {}) + -> /* topological_view yielding vertex_info */ +``` + +**Yields**: `vertex_info` in topological order (VId=void; IDs accessible via descriptor) + +#### 4.4.2 edges_topological_sort + +```cpp +template> +auto edges_topological_sort(G&& g, EVF&& evf = {}, Alloc alloc = {}) + -> /* topological_view yielding edge_info */ +``` + +**Yields**: `edge_info` (non-sourced) in topological order (VId=void; IDs accessible via descriptor) + +#### 4.4.3 sourced_edges_topological_sort + +```cpp +template> +auto sourced_edges_topological_sort(G&& g, EVF&& evf = {}, Alloc alloc = {}) + -> /* topological_view yielding sourced edge_info */ +``` + +**Yields**: `edge_info` in topological order (VId=void; source/target IDs accessible via descriptor) + +**Implementation**: Uses reverse DFS post-order (Kahn's algorithm alternative available). + +**File Location**: `include/graph/views/topological_sort.hpp` + +--- + +## 5. Implementation Phases + +### Phase 0: Info Struct Refactoring ✅ COMPLETE (2026-01-31) + +**Completion Date**: 2026-01-31 +**Duration**: 1 day +**Commit**: 330c7d8 "[views] Phase 0: Info struct refactoring complete" + +**Tasks Completed**: +1. ✅ Refactored vertex_info (4 specializations for VId × V × VV void combinations) +2. ✅ Refactored edge_info (8 specializations for Sourced × VId × E × EV void combinations) +3. ✅ Refactored neighbor_info (8 specializations for Sourced × VId × V × VV void combinations) +4. ✅ Implemented all 20 new VId=void specializations +5. ✅ Created comprehensive test suite (27 test cases, 392 assertions) + +**Key Discoveries**: +- edge_info uses `source_id` and `target_id` (vertex IDs), not `edge_id` +- neighbor_info uses `target` member when VId present, `vertex` when VId=void +- Sourced parameter controls presence of `source_id` member +- sizeof tests account for struct padding + +**Files Modified**: +- `include/graph/graph_info.hpp` - Added 20 new specializations +- `tests/views/test_info_structs.cpp` - Created comprehensive test suite + +--- + +### Phase 1: Foundation (2-3 days) + +**Tasks**: +1. Create `include/graph/views/` directory structure +2. Create `include/graph/views/search_base.hpp`: + - `cancel_search` enum + - `visited_tracker` template + - Common search state management +3. Verify/extend info structs in `graph_info.hpp` if needed +4. Create view concepts in `include/graph/views/view_concepts.hpp` + +**Files**: +- `include/graph/views/search_base.hpp` +- `include/graph/views/view_concepts.hpp` + +--- + +### Phase 2: Basic Views (3-4 days) + +**Tasks**: +1. Implement `vertexlist` view and tests +2. Implement `incidence` view and tests +3. Implement `neighbors` view and tests +4. Implement `edgelist` view and tests +5. Create convenience header `include/graph/views/basic_views.hpp` + +**Files**: +- `include/graph/views/vertexlist.hpp` +- `include/graph/views/incidence.hpp` +- `include/graph/views/neighbors.hpp` +- `include/graph/views/edgelist.hpp` +- `include/graph/views/basic_views.hpp` +- `tests/views/test_basic_views.cpp` + +--- + +### Phase 3: DFS Views (2-3 days) + +**Tasks**: +1. Implement DFS state management +2. Implement `vertices_dfs` view and tests +3. Implement `edges_dfs` view and tests +4. Implement `sourced_edges_dfs` view and tests +5. Test cancel functionality + +**Files**: +- `include/graph/views/dfs.hpp` +- `tests/views/test_dfs_views.cpp` + +--- + +### Phase 4: BFS Views (2-3 days) + +**Tasks**: +1. Implement BFS state management (queue-based) +2. Implement `vertices_bfs` view and tests +3. Implement `edges_bfs` view and tests +4. Implement `sourced_edges_bfs` view and tests +5. Test depth() and size() accessors + +**Files**: +- `include/graph/views/bfs.hpp` +- `tests/views/test_bfs_views.cpp` + +--- + +### Phase 5: Topological Sort Views (2 days) + +**Tasks**: +1. Implement topological sort algorithm +2. Implement all three view variants +3. Handle cycle detection (throw or return sentinel) +4. Test on various DAGs + +**Files**: +- `include/graph/views/topological_sort.hpp` +- `tests/views/test_topological_views.cpp` + +--- + +### Phase 6: Integration and Polish (1-2 days) + +**Tasks**: +1. Create unified header `include/graph/views.hpp` +2. Update `graph.hpp` to include views +3. Write documentation +4. Performance benchmarks +5. Edge cases and error handling + +**Files**: +- `include/graph/views.hpp` +- Update `include/graph/graph.hpp` +- `docs/views.md` +- `benchmark/benchmark_views.cpp` + +--- + +## 6. Design Decisions + +### 6.1 Value Functions (VVF/EVF) + +**Question**: Should value functions be stored in the view or applied at each dereference? + +**Decision**: Store in view, apply at dereference. This avoids redundant copies and allows +stateful value functions. + +### 6.2 Iterator Category + +**Question**: What iterator category should views provide? + +**Decision**: Forward iterators. Random access is not meaningful for search views, and +bidirectional adds complexity without clear benefit. + +### 6.3 State Sharing Between Iterators + +**Question**: How should search views handle iterator copies? + +**Decision**: Use `std::shared_ptr` for the internal state. Multiple iterators +from the same view share state. This is necessary because search views are not restartable. + +### 6.4 Allocator Support + +**Question**: Should views support custom allocators? + +**Decision**: Yes. Search views need stack/queue/visited storage. Pass allocator as last +parameter with default `std::allocator`. + +### 6.5 Const Correctness + +**Question**: How should const graphs behave? + +**Decision**: Views from `const G&` yield `const` references in info structs. The `vertex` +and `edge` members will be const references. + +### 6.6 Seed Parameter Overloads + +**Question**: Should search views accept vertex descriptors as well as vertex IDs for the seed? + +**Decision**: Yes. All search views (`vertices_dfs`, `edges_dfs`, `vertices_bfs`, `edges_bfs`, +`vertices_topological_sort`, `edges_topological_sort`) should provide overloads accepting either: +- `vertex_id_t` - The traditional vertex ID (integer index) +- `vertex_t` - A vertex descriptor + +**Rationale**: +1. Users iterating over vertices via `vertices()` CPO already have descriptors +2. Avoids unnecessary `vertex_id(g, v)` call at call site +3. Consistent with CPO design which accepts descriptors +4. View constructors that accept vertex_id should delegate to vertex_descriptor constructors + +**Implementation Pattern**: +```cpp +// Constructor accepting vertex descriptor (primary) +vertices_dfs_view(G& g, vertex_type seed_vertex, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, seed_vertex, num_vertices(g), alloc)) +{} + +// Constructor accepting vertex ID (delegates to descriptor constructor) +vertices_dfs_view(G& g, vertex_id_type seed, Alloc alloc = {}) + : vertices_dfs_view(g, *find_vertex(g, seed), alloc) +{} + +// Factory functions for both +template +auto vertices_dfs(G& g, vertex_id_t seed); // From vertex ID + +template +auto vertices_dfs(G& g, vertex_t seed_vertex); // From vertex descriptor +``` + +### 6.7 Freestanding Support + +**Question**: Should views support graphs without random-access vertices? + +**Decision**: Not for search views. DFS/BFS/topological require `index_adjacency_list` because +they need efficient visited tracking via vertex IDs as indices. Basic views (`vertexlist`, +`incidence`, `neighbors`) can work with non-indexed graphs. + +--- + +## 7. Testing Strategy + +### 7.1 Test Graphs + +Create test utilities with standard graph types: +- Empty graph +- Single vertex +- Path graph (1-2-3-...-n) +- Cycle graph +- Complete graph K_n +- Binary tree +- DAG (for topological sort) +- Disconnected graph (multiple components) + +### 7.2 Container Type Coverage + +Views must be tested with multiple container configurations to ensure they work correctly +with both index-based and iterator-based vertex/edge storage: + +**Vertex Container Types**: +- `vector` (vov, vol, etc.) - Random-access, contiguous vertex IDs starting at 0 +- `map` (mov, mol, etc.) - Sparse vertex IDs, iterator-based storage, sorted order +- `deque` (dov, dol, etc.) - Random-access but non-contiguous + +**Edge Container Types**: +- `vector` - Random-access edges +- `map` (voem, moem, etc.) - Sorted edges by target, deduplicated +- `list` - Forward-only edges + +**Test Matrix** (minimum coverage per view): +| View | vov | mov | voem | moem | +|------|-----|-----|------|------| +| vertexlist | ✓ | ✓ | ✓ | ✓ | +| incidence | ✓ | ✓ | ✓ | ✓ | +| neighbors | ✓ | ✓ | ✓ | ✓ | +| edgelist | ✓ | ✓ | ✓ | ✓ | + +**Key Differences to Test**: +- **Map vertices**: Sparse, non-contiguous IDs; `vertices()` returns iterator-based descriptors +- **Map edges**: Edges sorted by target_id; duplicate targets deduplicated +- **Descriptor storage**: For maps, `vertex_descriptor::storage_type` is an iterator, not an index + +### 7.3 Test Categories + +1. **Basic Functionality**: Verify correct elements are yielded +2. **Ordering**: DFS/BFS produce correct visit order +3. **Structured Bindings**: All info struct variants work with `auto&& [a, b, ...]` +4. **Value Functions**: Custom VVF/EVF produce expected values +5. **Search Control**: cancel_branch and cancel_all work correctly +6. **Depth/Size Tracking**: Accurate depth and count during traversal +7. **Const Correctness**: Views from const graphs compile and work +8. **Edge Cases**: Empty graphs, single vertex, disconnected components +9. **Container Variance**: Views work with map-based and vector-based containers + +### 7.4 Test File Organization + +``` +tests/views/ +├── test_basic_views.cpp +├── test_dfs_views.cpp +├── test_bfs_views.cpp +├── test_topological_views.cpp +└── test_view_integration.cpp +``` + +--- + +## 8. Info Struct Refactoring (Phase 0) ✅ COMPLETE + +**Completion Date**: 2026-01-31 +**Commit**: 330c7d8 "[views] Phase 0: Info struct refactoring complete" +**Test Results**: ✅ 27 test cases, 392 assertions, all passing + +The existing info structs in `graph_info.hpp` were designed for compatibility with the +reference-based graph-v2 model. They have been refactored to align with the descriptor-based +architecture of graph-v3. + +### 8.1 vertex_info Refactoring ✅ COMPLETE + +**Original Design** (graph-v2 compatible): +```cpp +template +struct vertex_info { + id_type id; // vertex_id_t - always required + vertex_type vertex; // vertex_t - optional (void to omit) + value_type value; // from vvf - optional (void to omit) +}; +``` + +**Problem**: Separate `id` and `vertex` members are redundant with descriptors. A vertex +descriptor already contains the ID (accessible via `vertex_id()` or CPO `vertex_id(g, desc)`). + +**However**: The `{id, value}` combination (`vertex_info`) remains useful for +external data scenarios, such as: +- Providing vertex data to graph constructors before the graph exists +- Exporting vertex data from one graph for use in another +- Serialization/deserialization where descriptors aren't available +- External algorithms that operate on ID-value pairs without graph context + +**New Design** (all members optional via void): +```cpp +template +struct vertex_info { + using id_type = VId; // vertex_id_t - optional (void to omit) + using vertex_type = V; // vertex_t or vertex_descriptor<...> - optional (void to omit) + using value_type = VV; // from vvf - optional (void to omit) + + id_type id; // Conditionally present when VId != void + vertex_type vertex; // Conditionally present when V != void + value_type value; // Conditionally present when VV != void +}; +// Specializations for void combinations follow... +``` + +**Key Changes**: +1. **All three template parameters can now be void** (previously only V and VV could be void) +2. **VId = void** suppresses the `id` member (useful when V is a descriptor that contains ID) +3. **V = void** suppresses the `vertex` member (useful for ID-only iteration) +4. **VV = void** suppresses the `value` member (useful when no value function provided) +5. This creates flexibility: with descriptors, `id` becomes redundant; without descriptors, `id` is essential + +**Usage Comparison**: +```cpp +// Current design (all members present): +for (auto&& [id, v, val] : vertexlist(g, vvf)) { + // id is vertex_id_t + // v is vertex_t (reference or descriptor) +} + +// With descriptor (VId=void, V=descriptor, VV present): +vertex_info, int> // only vertex and value members +for (auto&& [v, val] : vertexlist(g, vvf)) { + auto id = v.vertex_id(); // extract ID from descriptor +} + +// ID-only (VId present, V=void, VV=void): +vertex_info // only id member +for (auto&& [id] : vertexlist(g)) { + // Just the ID, no vertex reference or value +} + +// Descriptor-only (VId=void, V=descriptor, VV=void): +vertex_info, void> // only vertex member +for (auto&& [v] : vertexlist(g)) { + auto id = v.vertex_id(); +} +``` + +**Structured Binding Patterns**: +- `vertex_info`: `auto&& [id, vertex, value]` +- `vertex_info`: `auto&& [id, vertex]` +- `vertex_info`: `auto&& [id, value]` +- `vertex_info`: `auto&& [id]` +- `vertex_info`: `auto&& [vertex, value]` +- `vertex_info`: `auto&& [vertex]` +- `vertex_info`: `auto&& [value]` (unlikely use case) + +**Migration Helpers** (optional): +```cpp +// Convenience accessor if users frequently need just the ID +template +constexpr auto id(const vertex_info& vi) { + return vi.vertex.vertex_id(); +} +``` + +### 8.2 edge_info Refactoring ✅ COMPLETE + +**Original Design** (graph-v2 compatible): +```cpp +template +struct edge_info { + source_id_type source_id; // VId when Sourced==true, void otherwise + target_id_type target_id; // VId - always required + edge_type edge; // E - optional (void to omit) + value_type value; // from evf - optional (void to omit) +}; +// 8 specializations for Sourced × E × EV combinations +``` + +**Problem**: Separate `source_id`, `target_id`, and `edge` members are redundant with +edge descriptors. An edge descriptor already provides: +- `source_id()` - the source vertex ID +- `target_id(container)` - the target vertex ID +- Access to the underlying edge data + +**However**: The `{source_id, target_id, value}` combination (`edge_info`) +remains useful for external data scenarios, such as: +- Providing edge data to graph constructors before the graph exists +- Exporting edge lists from one graph for use in another +- Serialization/deserialization where descriptors aren't available +- External algorithms that operate on edge triples (src, tgt, value) without graph context +- CSV/JSON edge list import/export + +**New Design** (all members optional via void): +```cpp +template +struct edge_info { + using source_id_type = conditional_t; // optional based on Sourced + using target_id_type = VId; // optional (void to omit) + using edge_type = E; // optional (void to omit) + using value_type = EV; // optional (void to omit) + + source_id_type source_id; // Conditionally present when Sourced==true && VId != void + target_id_type target_id; // Conditionally present when VId != void + edge_type edge; // Conditionally present when E != void + value_type value; // Conditionally present when EV != void +}; +// Specializations for Sourced × void combinations follow... +``` + +**Key Changes**: +1. **VId can now be void** to suppress both `source_id` and `target_id` members +2. When VId=void and E=descriptor, IDs are accessible via `edge.source_id()` and `edge.target_id(g)` +3. **Sourced bool** determines if source_id is present (when VId != void) +4. **E = void** suppresses the `edge` member (for ID-only edge info) +5. **EV = void** suppresses the `value` member +6. Specialization count depends on combinations: Sourced × VId × E × EV + +**Usage Comparison**: +```cpp +// Current design with IDs (sourced): +for (auto&& [src, tgt, e, val] : sourced_edges(g, u, evf)) { + // src is source vertex ID + // tgt is target vertex ID + // e is edge_t +} + +// With descriptor (VId=void, Sourced=true, E=descriptor, EV present): +edge_info, int> // only edge and value members +for (auto&& [e, val] : incidence(g, u, evf)) { + auto src = e.source_id(); // extract from descriptor + auto tgt = e.target_id(g); // extract from descriptor +} + +// IDs-only (VId present, Sourced=true, E=void, EV=void): +edge_info // only source_id and target_id members +for (auto&& [src, tgt] : incidence(g, u)) { +} + +// New (graph-v3 descriptor style): +for (auto&& [e, val] : incidence(g, u, evf)) { + auto src = e.source_id(); // or source_id(g, e) + auto tgt = e.target_id(g); // or target_id(g, e) + // e is edge_descriptor<...> +} + +// Without value function: +for (auto&& [e] : incidence(g, u)) { + auto src = e.source_id(); + auto tgt = e.target_id(g); +} +``` + +**Structured Binding Impact**: +- Old: `auto&& [src, tgt, edge, value]` (sourced) or `auto&& [tgt, edge, value]` (non-sourced) + void variants +- New: `auto&& [edge, value]` or `auto&& [edge]` + +**Note on Sourced vs Non-Sourced**: The old `Sourced` bool is no longer needed because +edge descriptors in graph-v3 always carry their source vertex context. The distinction +between "sourced" and "non-sourced" edges is now a property of how the view is used, +not the info struct type. + +### 8.3 neighbor_info Refactoring ✅ COMPLETE + +**Original Design** (graph-v2 compatible): +```cpp +template +struct neighbor_info { + source_id_type source_id; // VId when Sourced==true, void otherwise + target_id_type target_id; // VId - always required + vertex_type target; // V (target vertex) - optional (void to omit) + value_type value; // from vvf - optional (void to omit) +}; +// 8 specializations for Sourced × V × VV combinations +``` + +**Problem**: The `neighbors` view iterates over edges but yields target vertex info. +Internally, the view has an edge descriptor for navigation (it knows source and target), +and it creates a `neighbor_info` for the target edge. The user primarily cares about the +target vertex, but the edge descriptor provides the necessary context for accessing it. + +**However**: Similar to vertex_info and edge_info, the `{source_id, target_id, value}` +combination (`neighbor_info`) remains useful for external data +scenarios where the graph isn't available. + +**New Design** (all members optional via void): +```cpp +template +struct neighbor_info { + using source_id_type = conditional_t; // optional based on Sourced + using target_id_type = VId; // optional (void to omit) + using vertex_type = V; // optional (void to omit) - typically vertex_descriptor<...> + using value_type = VV; // optional (void to omit) + + source_id_type source_id; // Conditionally present when Sourced==true && VId != void + target_id_type target_id; // Conditionally present when VId != void + vertex_type vertex; // Conditionally present when V != void + value_type value; // Conditionally present when VV != void +}; +// Specializations for Sourced × void combinations follow... +``` + +**IMPLEMENTATION NOTE** (as of 2026-01-31): +The actual implementation uses different member names based on VId: +- When **VId is present**: member is named `target` (not `vertex`) +- When **VId=void**: member is named `vertex` + +This naming distinction exists in the codebase and is reflected in Phase 0 tests. + +**Key Changes**: +1. **VId can now be void** to suppress both `source_id` and `target_id` members +2. When VId=void and V=vertex_descriptor, IDs are accessible via `vertex.vertex_id()` or CPO `vertex_id(g, vertex)` +3. **Sourced bool** determines if source_id is present (when VId != void) +4. **V = void** suppresses the `vertex` member (for ID-only neighbor iteration) +5. **VV = void** suppresses the `value` member +6. **Primary use case**: `neighbor_info, VV>` yields `{vertex, value}` for target vertex + +**Usage Comparison**: +```cpp +// Current design with IDs (sourced): +for (auto&& [src, tgt_id, tgt, val] : neighbors(g, u, vvf)) { + // src is source vertex ID + // tgt_id is target vertex ID + // tgt is target vertex reference +} + +// With vertex descriptor (VId=void, Sourced=false, V=vertex_descriptor, VV present): +neighbor_info, int> // {vertex, value} +for (auto&& [v, val] : neighbors(g, u, vvf)) { + auto tgt_id = v.vertex_id(); // extract ID from descriptor + auto& tgt_data = vertex_value(g, v); // access underlying vertex data + // v is the target vertex descriptor +} + +// Without value function (VId=void, Sourced=false, V=vertex_descriptor, VV=void): +neighbor_info, void> // {vertex} +for (auto&& [v] : neighbors(g, u)) { + auto tgt_id = v.vertex_id(); + // Just the vertex descriptor, no value projection +} + +// IDs-only for external data (VId present, Sourced=false, V=void, VV=void): +neighbor_info // {target_id} +for (auto&& [tgt_id] : neighbors(g, u)) { + // Just target ID, no descriptor or value +} + +// With IDs and value for external data (VId present, Sourced=true, V=void, VV present): +neighbor_info // {source_id, target_id, value} +for (auto&& [src, tgt, val] : neighbors(g, u, vvf)) { + // Useful for exporting edges with values +} +``` + +**Design Rationale**: +- **Primary pattern**: VId=void, Sourced=false, V=vertex_descriptor yields `{vertex, value}` +- Vertex descriptor provides access to vertex ID and underlying data +- VId present with V=void supports external data scenarios (export, serialization) +- Sourced=true adds source_id when VId != void (for edge-like neighbor info) +- Consistent with vertex_info and edge_info optional-members design + +### 8.4 Implementation Summary ✅ COMPLETE + +**Phase 0 was completed in three steps:** + +**Step 0.1: vertex_info Refactoring** ✅ +1. ✅ Made VId template parameter optional (void to suppress id member) +2. ✅ Ensured all three members (id, vertex, value) can be conditionally present +3. ✅ Updated specializations to handle all void combinations (4 specializations) +4. ✅ Kept `copyable_vertex_t` = `vertex_info` alias +5. ✅ Created comprehensive tests + +**Step 0.2: edge_info Refactoring** ✅ +1. ✅ Made VId template parameter optional (void to suppress source_id/target_id members) +2. ✅ Ensured all four members (source_id, target_id, edge, value) can be conditionally present +3. ✅ Updated specializations to handle Sourced × void combinations (8 specializations) +4. ✅ Kept `copyable_edge_t` = `edge_info` alias +5. ✅ Kept `is_sourced_v` trait +6. ✅ Created comprehensive tests + +**Step 0.3: neighbor_info Refactoring** ✅ +1. ✅ Made VId template parameter optional (void to suppress source_id/target_id members) +2. ✅ Ensured all four members (source_id, target_id, vertex, value) can be conditionally present +3. ✅ Updated specializations to handle Sourced × void combinations (8 specializations) +4. ✅ Implemented naming convention: `target` when VId present, `vertex` when VId=void +5. ✅ Kept `copyable_neighbor_t` alias +6. ✅ Kept `is_sourced_v` trait for neighbor_info +7. ✅ Created comprehensive tests + +### 8.5 Summary of Info Struct Changes + +**Key Insight**: All members of info structs are now **optional via void template parameters**. +This provides maximum flexibility for different use cases: +- **With descriptors**: VId can be void (descriptor contains ID) +- **Without descriptors**: VId is required, V/E can be void for lightweight iteration +- **Copyable types**: Use `copyable_vertex_t` = `vertex_info` + +| Struct | Template Parameters | Members (when not void) | Flexibility | +|--------|---------------------|-------------------------|-------------| +| `vertex_info` | `` | `id`, `vertex`, `value` | All 3 can be void | +| `edge_info` | `` | `source_id`, `target_id`, `edge`, `value` | VId, E, EV can be void | +| `neighbor_info` | `` | `source_id`, `target_id`, `target`/`vertex`*, `value` | VId, V, VV can be void | + +*`neighbor_info` uses `target` when VId present, `vertex` when VId=void (implementation detail) + +**Primary Usage Patterns**: +- **vertex_info**: `vertex_info, VV>` → `{vertex, value}` +- **edge_info**: `edge_info, EV>` → `{edge, value}` (Sourced=false when VId=void; source accessible via descriptor) +- **neighbor_info**: `neighbor_info, VV>` → `{vertex, value}` (Sourced=false; target vertex only) + +**Specialization Impact**: +- `vertex_info`: 8 specializations (2³ void combinations: VId, V, VV) +- `edge_info`: 16 specializations (2 Sourced × 2³ void combinations: VId, E, EV) +- `neighbor_info`: 16 specializations (2 Sourced × 2³ void combinations: VId, V, VV) +- **Total**: 40 specializations (implementation can reduce with SFINAE or conditional members) + +**Implementation Status** (as of 2026-01-31): +- ✅ **Phase 0 Complete**: All 40 specializations implemented in `graph_info.hpp` +- ✅ **Tests**: 27 test cases, 392 assertions, all passing +- ✅ **Commit**: 330c7d8 "[views] Phase 0: Info struct refactoring complete" + +--- + +## 9. Descriptor-Based Architecture Considerations + +The graph-v3 library uses a **value-based descriptor model** rather than the reference-based +model in graph-v2. This section covers additional implementation concerns beyond the info +struct refactoring in Section 8. + +### 9.1 What Are Descriptors? + +In graph-v3: +- `vertex_descriptor` wraps either an index (`size_t`) or an iterator +- `edge_descriptor` wraps edge storage + source vertex descriptor +- Descriptors are **lightweight value types** (copyable, comparable) +- Descriptors require the container/graph reference to access underlying data + +```cpp +// Descriptor is a handle, not the data itself +vertex_descriptor desc(idx); // Just stores the index +auto& vertex_data = desc.underlying_value(container); // Needs container to access data +``` + +### 9.2 Edge Descriptor Source Context + +**Note**: `edge_descriptor` contains a source vertex descriptor as a member variable, +providing complete source vertex context. This design simplifies edge-yielding views: + +1. **edgelist view**: When flattening `for u: for e in edges(g,u)`, the edge descriptor + created from the edge iterator naturally captures the source vertex. + +2. **Search views returning edges**: DFS/BFS edge views create edge descriptors that + inherently contain the source vertex context. + +**Implementation**: Views that yield edges create descriptors with source context: + +```cpp +class edgelist_iterator { + G* g_; + vertex_descriptor<...> current_source_; // Current source vertex descriptor + edge_iterator_t current_edge_; + edge_iterator_t edge_end_; + + auto operator*() const { + // Construct edge descriptor with source vertex descriptor + auto edge_desc = edge_descriptor{current_edge_, current_source_}; + return edge_info{edge_desc, ...}; + } +}; +``` + +The source vertex descriptor member in `edge_descriptor` eliminates the need for separate +source ID tracking—it's built into the descriptor itself. + +### 9.3 Value Function Design + +**Problem**: Should value functions receive descriptors or underlying values? + +**In graph-v2 (reference-based)**: +```cpp +vvf(vertex_reference) // Direct access to vertex data +``` + +**In graph-v3 (descriptor-based)**: +```cpp +// Option A: Pass descriptor +vvf(vertex_descriptor) // User can access ID, underlying value, etc. + +// Option B: Pass descriptor + graph +vvf(g, vertex_descriptor) // View passes both (redundant - descriptor works with graph already) + +// Option C: Pass underlying value (view extracts it) +vvf(underlying_vertex_value) // View does: vvf(vertex_value(g, v)) +``` + +**Decision**: **Option A** - Value functions receive descriptors. + +Descriptors provide complete access while maintaining the value-based semantics: +```cpp +// Vertex value function receives vertex_descriptor +for (auto&& [v, val] : vertexlist(g, [&g](auto vdesc) { + return vertex_value(g, vdesc).name; // Access underlying value via descriptor +})) { + // val is the extracted name +} + +// Edge value function receives edge_descriptor +for (auto&& [e, val] : incidence(g, u, [&g](auto edesc) { + return edge_value(g, edesc).weight; // Access underlying edge value +})) { + // val is the extracted weight +} + +// Neighbor value function receives target vertex_descriptor +for (auto&& [v, val] : neighbors(g, u, [&g](auto vdesc) { + return vertex_value(g, vdesc).name; // Access target vertex value +})) { + // v is target vertex descriptor, val is extracted name +} +``` + +**Rationale**: +- Descriptors are lightweight values that can be copied efficiently +- Value functions can access IDs, underlying values, or both as needed +- Consistent with descriptor-based architecture throughout graph-v3 +- Allows value functions to perform lookups or computations using the descriptor +- User captures graph reference once in the lambda, not passed separately + +### 9.4 Search State Storage + +**Problem**: What should DFS/BFS stacks/queues store? + +| Storage Type | Memory | Resumption | Visited Check | +|--------------|--------|------------|---------------| +| Vertex ID only | Minimal | Need to restart edge iteration | O(1) with ID-indexed vector | +| Vertex ID + Edge Iterator | Larger | Can resume mid-adjacency | O(1) | +| Full Descriptor | Larger | Can resume | O(1) but wasteful | + +**Decision**: Store `(vertex_id, edge_iterator)` pairs for DFS (need to resume +after recursion), store `vertex_id` only for BFS (process all neighbors at once). + +```cpp +// DFS state - needs to resume edge iteration +struct dfs_frame { + vertex_id_t vertex; + edge_iterator_t edge_iter; + edge_iterator_t edge_end; +}; + +// BFS state - process all neighbors when dequeued +using bfs_queue_entry = vertex_id_t; +``` + +### 9.5 Iterator Stability and Invalidation + +**Problem**: Descriptors using iterator storage (for non-random-access containers) can be +invalidated if the graph is mutated. + +**Rules**: +1. Views do not own data; graph must outlive view +2. Graph mutation during iteration is undefined behavior +3. Index-based descriptors (from vector storage) are stable unless reallocation occurs +4. Iterator-based descriptors are invalidated by any mutation + +**Documentation Required**: Views must document that: +- The graph reference must remain valid for the view's lifetime +- Mutating the graph during iteration is undefined behavior +- Copying info structs is safe (descriptors are values) + +### 9.6 Const Correctness with Descriptors + +**Problem**: How do descriptors from `const G&` differ from `G&`? + +In graph-v3, descriptors themselves don't carry const-ness—they're just handles. The +const-ness comes from the container access: + +```cpp +vertex_descriptor desc(idx); +auto& v = desc.underlying_value(container); // non-const reference +auto& v = desc.underlying_value(const_container); // const reference +``` + +**For views**: When constructed from `const G&`: +- Descriptors are the same type (values) +- Value functions receive `const` references to underlying data +- Info struct value members will be const if extracted from const graph + +### 9.7 Summary: Descriptor-Aware Design Decisions + +| Aspect | Decision | Rationale | +|--------|----------|-----------| +| Info struct members | Single descriptor + optional value (see Section 8) | Simplified, descriptor-native design | +| Value function input | Underlying value, not descriptor | User-friendly, matches expectations | +| View internal storage | IDs for visited, (ID, iterator) pairs for stack | Minimal memory, efficient operations | +| Edge source tracking | View maintains current source context | Required for edge descriptor construction | +| Const handling | Descriptors same type; const applied at access | Matches C++ const semantics | +| Iterator stability | Document UB on mutation | Standard range semantics | + +--- + +## 10. Open Questions + +1. **~~Should `edgelist` use edge_list CPO or iterate adjacency list?~~** *(Resolved)* + - **Decision**: Use concepts to detect graph type and dispatch appropriately: + - If `edge_list` is satisfied → use `edges(g)` CPO for direct edge iteration + - Else if `adjacency_list` is satisfied → flatten via `for u: for e in edges(g,u)` + - Implementation uses `if constexpr` with concept checks: + ```cpp + template + auto edgelist(G&& g, EVF&& evf = {}) { + if constexpr (edge_list>) { + return edgelist_from_edge_list(std::forward(g), std::forward(evf)); + } else { + static_assert(adjacency_list>); + return edgelist_from_adjacency_list(std::forward(g), std::forward(evf)); + } + } + ``` + +2. **Multi-source search views?** + - D3129 shows single-source. Should we support `vertices_bfs(g, {seeds...})`? + - **Design**: Use concepts to disambiguate single vs multi-seed overloads: + + ```cpp + // Single seed overloads + template + auto vertices_bfs(G&& g, vertex_id_t seed); + + template + requires std::invocable> + auto vertices_bfs(G&& g, vertex_id_t seed, VVF&& vvf); + + // Multi-seed overloads + template + requires std::convertible_to, vertex_id_t> + auto vertices_bfs(G&& g, Seeds&& seeds); + + template + requires std::convertible_to, vertex_id_t> + && std::invocable> + auto vertices_bfs(G&& g, Seeds&& seeds, VVF&& vvf); + ``` + + **Disambiguation**: + - Second arg is scalar `vertex_id_t` → single seed + - Second arg is range of `vertex_id_t` → multi-seed + - Third arg (vvf) is always optional, same type for both + + **Usage**: + ```cpp + vertices_bfs(g, 0) // single seed, no vvf + vertices_bfs(g, 0, my_vvf) // single seed, with vvf + vertices_bfs(g, std::vector{0, 3, 7}) // multi-seed, no vvf + vertices_bfs(g, std::vector{0, 3, 7}, my_vvf) // multi-seed, with vvf + vertices_bfs(g, std::array{0, 3, 7}, my_vvf) // also works with array + ``` + + - Can defer implementation to later phase, but design accommodates it. + + **Apply same overload shape to other search views** (with appropriate value functions): + - `vertices_dfs`: identical constraints with `VVF` on `vertex_descriptor_t` + - `edges_*` / `sourced_edges_*` variants: use `EVF` with `std::invocable>` and seed(s) as above + - **Note**: `vertices_topological_sort` does NOT take seed parameter(s); it processes all vertices in the DAG + +3. **Parallel/concurrent views?** *(Deferred)* + - Out of scope for initial implementation. + - Design should not preclude future parallelization. + +4. **Range adaptor syntax?** *(Supported)* + - **Decision**: Support `g | view_name(args...)` pipe syntax for all views. + - **Design**: Use range adaptor closure pattern from C++23 (backportable to C++20): + + ```cpp + namespace graph::views { + + // Adaptor closure object for vertexlist + template + struct vertexlist_adaptor { + VVF vvf; + + template + friend auto operator|(G&& g, vertexlist_adaptor adaptor) { + if constexpr (std::is_void_v) { + return vertexlist(std::forward(g)); + } else { + return vertexlist(std::forward(g), std::move(adaptor.vvf)); + } + } + }; + + // Factory function + inline constexpr auto vertexlist = [](VVF&& vvf = {}) { + return vertexlist_adaptor>{std::forward(vvf)}; + }; + + // Adaptor closure for incidence + template + struct incidence_adaptor { + vertex_id_t_placeholder uid; + EVF evf; + + template + friend auto operator|(G&& g, incidence_adaptor adaptor) { + if constexpr (std::is_void_v) { + return incidence(std::forward(g), adaptor.uid); + } else { + return incidence(std::forward(g), adaptor.uid, std::move(adaptor.evf)); + } + } + }; + inline constexpr auto incidence = [](auto uid, EVF&& evf = {}) { + return incidence_adaptor>{uid, std::forward(evf)}; + }; + + // Adaptor closure for neighbors + template + struct neighbors_adaptor { + vertex_id_t_placeholder uid; + VVF vvf; + + template + friend auto operator|(G&& g, neighbors_adaptor adaptor) { + if constexpr (std::is_void_v) { + return neighbors(std::forward(g), adaptor.uid); + } else { + return neighbors(std::forward(g), adaptor.uid, std::move(adaptor.vvf)); + } + } + }; + inline constexpr auto neighbors = [](auto uid, VVF&& vvf = {}) { + return neighbors_adaptor>{uid, std::forward(vvf)}; + }; + + // Adaptor closure for edgelist + template + struct edgelist_adaptor { + EVF evf; + template + friend auto operator|(G&& g, edgelist_adaptor adaptor) { + if constexpr (std::is_void_v) { + return edgelist(std::forward(g)); + } else { + return edgelist(std::forward(g), std::move(adaptor.evf)); + } + } + }; + inline constexpr auto edgelist = [](EVF&& evf = {}) { + return edgelist_adaptor>{std::forward(evf)}; + }; + + // Search view adaptors (single-seed; multi-seed mirror these with a range Seed type) + template + struct vertices_bfs_adaptor { + Seed seed; + VVF vvf; + + template + friend auto operator|(G&& g, vertices_bfs_adaptor adaptor) { + if constexpr (std::is_void_v) { + return vertices_bfs(std::forward(g), std::move(adaptor.seed)); + } else { + return vertices_bfs(std::forward(g), std::move(adaptor.seed), + std::move(adaptor.vvf)); + } + } + }; + + template + struct vertices_dfs_adaptor { + Seed seed; + VVF vvf; + + template + friend auto operator|(G&& g, vertices_dfs_adaptor adaptor) { + if constexpr (std::is_void_v) { + return vertices_dfs(std::forward(g), std::move(adaptor.seed)); + } else { + return vertices_dfs(std::forward(g), std::move(adaptor.seed), + std::move(adaptor.vvf)); + } + } + }; + + template + struct vertices_topo_adaptor { + VVF vvf; + + template + friend auto operator|(G&& g, vertices_topo_adaptor adaptor) { + if constexpr (std::is_void_v) { + return vertices_topological_sort(std::forward(g)); + } else { + return vertices_topological_sort(std::forward(g), + std::move(adaptor.vvf)); + } + } + }; + + // Edge-yielding search adaptors (EVF on edge values) + template + struct edges_bfs_adaptor { + Seed seed; + EVF evf; + + template + friend auto operator|(G&& g, edges_bfs_adaptor adaptor) { + if constexpr (std::is_void_v) { + return edges_bfs(std::forward(g), std::move(adaptor.seed)); + } else { + return edges_bfs(std::forward(g), std::move(adaptor.seed), + std::move(adaptor.evf)); + } + } + }; + + template + struct sourced_edges_bfs_adaptor { + Seed seed; + EVF evf; + + template + friend auto operator|(G&& g, sourced_edges_bfs_adaptor adaptor) { + if constexpr (std::is_void_v) { + return sourced_edges_bfs(std::forward(g), std::move(adaptor.seed)); + } else { + return sourced_edges_bfs(std::forward(g), std::move(adaptor.seed), + std::move(adaptor.evf)); + } + } + }; + + // Factories for search views + inline constexpr auto vertices_bfs = [](Seed&& seed, VVF&& vvf = {}) { + return vertices_bfs_adaptor, std::decay_t>{std::forward(seed), std::forward(vvf)}; + }; + inline constexpr auto vertices_dfs = [](Seed&& seed, VVF&& vvf = {}) { + return vertices_dfs_adaptor, std::decay_t>{std::forward(seed), std::forward(vvf)}; + }; + inline constexpr auto vertices_topological_sort = [](VVF&& vvf = {}) { + return vertices_topo_adaptor>{std::forward(vvf)}; + }; + inline constexpr auto edges_bfs = [](Seed&& seed, EVF&& evf = {}) { + return edges_bfs_adaptor, std::decay_t>{std::forward(seed), std::forward(evf)}; + }; + inline constexpr auto sourced_edges_bfs = [](Seed&& seed, EVF&& evf = {}) { + return sourced_edges_bfs_adaptor, std::decay_t>{std::forward(seed), std::forward(evf)}; + }; + + // DFS edge variants mirror BFS ones; topo edge variants mirror topo vertices + + } // namespace graph::views + ``` + + - Multi-seed adaptor closures mirror the single-seed versions with `Seeds` range parameters. + - `vertex_id_t_placeholder` in the code sketches above represents the vertex ID type (actual implementations use template deduction). + - `VVF`/`EVF` used in adaptors follow the same const-qualified constraints as the overloads above. + + **Usage**: + ```cpp + using namespace graph::views; + + // Basic views + for (auto&& [v] : g | vertexlist()) { ... } + for (auto&& [v, val] : g | vertexlist(my_vvf)) { ... } + for (auto&& [e] : g | incidence(uid)) { ... } + for (auto&& [e] : g | edgelist()) { ... } + + // Search views + for (auto&& [v] : g | vertices_bfs(seed)) { ... } + for (auto&& [v, val] : g | vertices_dfs(seed, [&g](auto vdesc) { return vertex_value(g, vdesc).name; })) { ... } + for (auto&& [v] : g | vertices_topological_sort()) { ... } + + // Chaining with standard views + auto names = g | vertexlist(get_name) | std::views::take(10); + ``` + + **Implementation Notes**: + - Adaptor closures are lightweight objects (store only captured args) + - `operator|` is a hidden friend for ADL + - No-arg adaptors (like `vertexlist()`) can be constexpr objects + - Compatible with standard library range adaptors for chaining + +--- + +## 11. Dependencies + +### External +- C++20 concepts, ranges +- Standard library containers (vector, stack, queue) + +### Internal +- `graph_info.hpp` - info structs +- `adjacency_list_concepts.hpp` - graph concepts +- Graph CPOs (vertices, edges, vertex, target_id, etc.) + +--- + +## 12. Success Criteria + +1. All basic views work with `index_adjacency_list` graphs +2. All search views correctly traverse and yield elements +3. Structured bindings work for all info struct variants +4. Value functions correctly transform yielded values +5. Search control (cancel) works as specified +6. All tests pass with both vector-based and deque-based graphs +7. Documentation covers all public API +8. No memory leaks (sanitizer clean) + +--- + +## 13. File Structure Summary + +``` +include/graph/ +├── views/ +│ ├── basic_views.hpp # Convenience header for basic views +│ ├── vertexlist.hpp # vertexlist view +│ ├── incidence.hpp # incidence view +│ ├── neighbors.hpp # neighbors view +│ ├── edgelist.hpp # edgelist view +│ ├── search_base.hpp # Common search infrastructure +│ ├── view_concepts.hpp # View-related concepts +│ ├── dfs.hpp # DFS views (vertices_dfs, edges_dfs, sourced_edges_dfs) +│ ├── bfs.hpp # BFS views (vertices_bfs, edges_bfs, sourced_edges_bfs) +│ └── topological_sort.hpp # Topological sort views +├── views.hpp # Master include for all views +└── graph.hpp # Updated to include views.hpp + +tests/views/ +├── test_basic_views.cpp +├── test_dfs_views.cpp +├── test_bfs_views.cpp +├── test_topological_views.cpp +└── test_view_integration.cpp +``` diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index f14bca9..2821a69 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -22,6 +22,7 @@ FetchContent_MakeAvailable(benchmark) add_executable(graph_benchmarks benchmark_main.cpp benchmark_vertex_access.cpp + benchmark_views.cpp ) target_link_libraries(graph_benchmarks diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..7d1f529 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,197 @@ +# Graph Library Benchmarks + +This directory contains performance benchmarks for the graph library components. + +## Running Benchmarks + +The project must be configured with benchmarks enabled: + +```bash +cmake -DBUILD_BENCHMARKS=ON -S . -B build/release +cmake --build build/release --target graph_benchmarks +``` + +Run all benchmarks: +```bash +./build/release/benchmark/graph_benchmarks +``` + +Run specific benchmarks: +```bash +./build/release/benchmark/graph_benchmarks --benchmark_filter="BM_DFS.*" +``` + +Run with minimum time per benchmark: +```bash +./build/release/benchmark/graph_benchmarks --benchmark_min_time=0.5s +``` + +## Benchmark Files + +### benchmark_vertex_access.cpp +Benchmarks for vertex descriptor operations: +- Vertex descriptor creation +- Vertex descriptor view iteration +- Comparison with raw iteration + +### benchmark_views.cpp +Comprehensive benchmarks for all graph views: +- **Basic Views**: vertexlist, incidence, neighbors, edgelist +- **Search Views**: DFS, BFS, topological sort (vertices and edges) +- **Comparison**: View iteration vs manual iteration +- **Chaining**: Integration with std::views (filter, transform, take) +- **Graph Types**: Random graphs, path graphs, complete graphs, DAGs + +## Performance Results + +### Basic Views + +All basic views demonstrate **O(n)** or **O(V + E)** complexity as expected: + +``` +BM_Vertexlist_Iteration_BigO 0.08 N (linear, minimal overhead) +BM_Edgelist_Iteration_BigO ~O(V+E) (visits all edges) +``` + +The overhead of using views compared to manual iteration is **negligible** (typically < 5%). + +### Search Views + +Search algorithms maintain their expected complexity: + +``` +BM_DFS_Vertices_BigO O(V + E) (standard DFS complexity) +BM_BFS_Vertices_BigO O(V + E) (standard BFS complexity) +BM_TopoSort_Vertices_BigO 7.84 N (linear in vertices) +BM_TopoSort_Edges_BigO 10.00 N (linear in edges) +``` + +#### BFS vs DFS Performance + +On random graphs (avg degree = 5): +- **BFS** is typically **faster** than DFS for small to medium graphs +- **DFS** performance degrades more on dense graphs (more recursion overhead) + +Example results (100 vertices): +``` +BM_DFS_Vertices/100 1002 ns +BM_BFS_Vertices/100 629 ns (38% faster) +``` + +### Graph Type Effects + +#### Path Graphs (Linear Structure) +Best case for both DFS and BFS: +``` +BM_DFS_PathGraph: Linear traversal, minimal backtracking +BM_BFS_PathGraph: Linear traversal, minimal queue operations +``` + +#### Complete Graphs (Dense) +Worst case - exponential edges: +``` +BM_DFS_CompleteGraph: O(n²) edges to traverse +BM_BFS_CompleteGraph: O(n²) edges, large queue +``` + +### View Chaining + +Chaining with standard library views adds minimal overhead: +``` +BM_Chaining_Filter: ~10% overhead (filter logic dominates) +BM_Chaining_Transform: ~5% overhead +BM_Chaining_Take: ~2% overhead (early termination benefit) +``` + +### Memory Characteristics + +- **Basic Views**: Zero-cost abstractions, no allocation +- **DFS/BFS**: O(V) visited set allocation +- **Topological Sort**: O(V) for visited set + O(V) for result vector +- **Chaining**: Lazy evaluation, no intermediate containers + +## Comparison: Views vs Manual Iteration + +Views have **near-zero overhead** compared to manual iteration: + +``` +BM_Manual_Vertices: baseline +BM_Vertexlist: +1-2% overhead (range protocol) + +BM_Manual_Edges: baseline +BM_Edgelist: +2-3% overhead (edge tuple creation) +``` + +The abstraction cost is **negligible**, making views an excellent choice for both performance and expressiveness. + +## Recommendations + +### Performance Best Practices + +1. **Use Basic Views for Simple Iteration** + - `vertexlist()`, `edgelist()`, `neighbors()` have minimal overhead + - As fast as manual loops with better expressiveness + +2. **Prefer BFS for Breadth-First Exploration** + - Generally faster than DFS on random and sparse graphs + - Better cache locality for level-order traversal + +3. **Use DFS for Depth-First Problems** + - Better for problems requiring complete path exploration + - Lower memory usage (no queue) + +4. **Chain with std::views Freely** + - Overhead is minimal compared to algorithmic cost + - Lazy evaluation prevents unnecessary work + +5. **Consider Graph Density** + - Dense graphs (complete, near-complete): expect O(V²) behavior + - Sparse graphs (random, trees): expect O(V + E) ≈ O(V) behavior + +### When to Optimize + +Views are typically **not** the bottleneck. Consider optimizing if: +- Graph algorithms themselves are O(V²) or higher +- Value functions are expensive +- Custom allocators would reduce allocation overhead +- Working with extremely large graphs (> 10M vertices) + +For typical use cases (< 100K vertices), views provide excellent performance with superior code clarity. + +## Benchmark Framework + +Benchmarks use [Google Benchmark](https://github.com/google/benchmark) (v1.9.1): + +- **Range Parameterization**: Tests across different graph sizes +- **Complexity Analysis**: Automatic O(N) analysis with `->Complexity()` +- **DoNotOptimize**: Prevents compiler from optimizing away work +- **Statistical Rigor**: Multiple iterations, outlier removal + +## Adding New Benchmarks + +Follow this template: + +```cpp +static void BM_YourBenchmark(benchmark::State& state) { + auto g = create_test_graph(state.range(0)); // Parameterized size + + for (auto _ : state) { + // Code to benchmark + for (auto [v] : g | your_view()) { + benchmark::DoNotOptimize(v); // Prevent optimization + } + } + + state.SetComplexityN(state.range(0)); // For complexity analysis +} +BENCHMARK(BM_YourBenchmark) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); +``` + +## Further Reading + +- [Google Benchmark User Guide](https://github.com/google/benchmark/blob/main/docs/user_guide.md) +- [C++20 Ranges Performance](https://www.youtube.com/watch?v=d_E-VLyUnzc) +- [Graph Algorithm Complexity](https://en.wikipedia.org/wiki/Time_complexity#Table_of_common_time_complexities) diff --git a/benchmark/benchmark_vertex_access.cpp b/benchmark/benchmark_vertex_access.cpp index 48a3e96..c3042eb 100644 --- a/benchmark/benchmark_vertex_access.cpp +++ b/benchmark/benchmark_vertex_access.cpp @@ -5,7 +5,7 @@ #include using namespace std; -using namespace graph; +using namespace graph::adj_list; // Benchmark vertex_descriptor creation with vector static void BM_VertexDescriptor_Vector_Creation(benchmark::State& state) { diff --git a/benchmark/benchmark_views.cpp b/benchmark/benchmark_views.cpp new file mode 100644 index 0000000..0d72f48 --- /dev/null +++ b/benchmark/benchmark_views.cpp @@ -0,0 +1,520 @@ +/** + * @file benchmark_views.cpp + * @brief Performance benchmarks for graph views + * + * Benchmarks measure iteration performance for all view types: + * - Basic views (vertexlist, incidence, neighbors, edgelist) + * - Search views (DFS, BFS, topological sort) + * - Comparison with manual iteration where applicable + */ + +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views::adaptors; + +// Test graph type: vector-of-vectors (adjacency list) +using TestGraph = std::vector>; + +// Create a random directed graph +TestGraph create_random_graph(size_t num_vertices, size_t avg_degree) { + TestGraph g(num_vertices); + std::mt19937 rng(42); // Fixed seed for reproducibility + + std::uniform_int_distribution dist(0, num_vertices - 1); + + for (size_t u = 0; u < num_vertices; ++u) { + size_t degree = std::poisson_distribution(avg_degree)(rng); + for (size_t i = 0; i < degree; ++i) { + size_t v = dist(rng); + if (v != u) { // Avoid self-loops + g[u].push_back(static_cast(v)); + } + } + } + + return g; +} + +// Create a path graph (0 -> 1 -> 2 -> ... -> n-1) +TestGraph create_path_graph(size_t num_vertices) { + TestGraph g(num_vertices); + for (size_t i = 0; i + 1 < num_vertices; ++i) { + g[i].push_back(static_cast(i + 1)); + } + return g; +} + +// Create a complete graph (all vertices connected) +TestGraph create_complete_graph(size_t num_vertices) { + TestGraph g(num_vertices); + for (size_t u = 0; u < num_vertices; ++u) { + for (size_t v = 0; v < num_vertices; ++v) { + if (u != v) { + g[u].push_back(static_cast(v)); + } + } + } + return g; +} + +// Create a DAG (directed acyclic graph) for topological sort +TestGraph create_dag(size_t num_vertices) { + TestGraph g(num_vertices); + for (size_t u = 0; u < num_vertices; ++u) { + // Connect to next few vertices only (maintains DAG property) + for (size_t v = u + 1; v < std::min(u + 5, num_vertices); ++v) { + g[u].push_back(static_cast(v)); + } + } + return g; +} + +//============================================================================= +// Basic Views Benchmarks +//============================================================================= + +// Benchmark: vertexlist iteration +static void BM_Vertexlist_Iteration(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertexlist()) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Vertexlist_Iteration) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: vertexlist with value function +static void BM_Vertexlist_WithValueFunction(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + auto vvf = [&g](auto v) { return vertex_id(g, v); }; + + for (auto _ : state) { + size_t sum = 0; + for (auto [v, id] : g | vertexlist(vvf)) { + benchmark::DoNotOptimize(v); + sum += id; + } + benchmark::DoNotOptimize(sum); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Vertexlist_WithValueFunction) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: incidence iteration (all vertices) +static void BM_Incidence_AllVertices(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (size_t u = 0; u < g.size(); ++u) { + for (auto [e] : g | incidence(u)) { + benchmark::DoNotOptimize(e); + ++count; + } + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Incidence_AllVertices) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: neighbors iteration (all vertices) +static void BM_Neighbors_AllVertices(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (size_t u = 0; u < g.size(); ++u) { + for (auto [n] : g | neighbors(u)) { + benchmark::DoNotOptimize(n); + ++count; + } + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Neighbors_AllVertices) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: edgelist iteration +static void BM_Edgelist_Iteration(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (auto [e] : g | edgelist()) { + benchmark::DoNotOptimize(e); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Edgelist_Iteration) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +//============================================================================= +// Search Views Benchmarks +//============================================================================= + +// Benchmark: DFS vertices traversal +static void BM_DFS_Vertices(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_dfs(0)) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_DFS_Vertices) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: DFS edges traversal +static void BM_DFS_Edges(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (auto [e] : g | edges_dfs(0)) { + benchmark::DoNotOptimize(e); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_DFS_Edges) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: BFS vertices traversal +static void BM_BFS_Vertices(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_bfs(0)) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_BFS_Vertices) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: BFS edges traversal +static void BM_BFS_Edges(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (auto [e] : g | edges_bfs(0)) { + benchmark::DoNotOptimize(e); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_BFS_Edges) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: Topological sort vertices +static void BM_TopoSort_Vertices(benchmark::State& state) { + auto g = create_dag(state.range(0)); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_topological_sort()) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_TopoSort_Vertices) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: Topological sort edges +static void BM_TopoSort_Edges(benchmark::State& state) { + auto g = create_dag(state.range(0)); + + for (auto _ : state) { + size_t count = 0; + for (auto [e] : g | edges_topological_sort()) { + benchmark::DoNotOptimize(e); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_TopoSort_Edges) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +//============================================================================= +// Comparison Benchmarks (View vs Manual) +//============================================================================= + +// Benchmark: Manual vertex iteration (baseline) +static void BM_Manual_Vertices(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (size_t i = 0; i < g.size(); ++i) { + benchmark::DoNotOptimize(i); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Manual_Vertices) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: Manual edge iteration (baseline) +static void BM_Manual_Edges(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + for (size_t u = 0; u < g.size(); ++u) { + for (const auto& v : g[u]) { + benchmark::DoNotOptimize(u); + benchmark::DoNotOptimize(v); + ++count; + } + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Manual_Edges) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +//============================================================================= +// Chaining Benchmarks +//============================================================================= + +// Benchmark: View chaining with std::views::filter +static void BM_Chaining_Filter(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + auto filtered = g + | vertexlist() + | std::views::filter([&g](auto info) { + auto [v] = info; + return vertex_id(g, v) % 2 == 0; + }); + + for (auto info : filtered) { + benchmark::DoNotOptimize(info); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Chaining_Filter) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: View chaining with std::views::transform +static void BM_Chaining_Transform(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t sum = 0; + auto transformed = g + | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }); + + for (auto id : transformed) { + sum += id; + } + benchmark::DoNotOptimize(sum); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Chaining_Transform) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: View chaining with std::views::take +static void BM_Chaining_Take(benchmark::State& state) { + auto g = create_random_graph(state.range(0), 5); + + for (auto _ : state) { + size_t count = 0; + auto limited = g + | vertexlist() + | std::views::take(100); // Take first 100 + + for (auto info : limited) { + benchmark::DoNotOptimize(info); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_Chaining_Take) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +//============================================================================= +// Graph Type Benchmarks +//============================================================================= + +// Benchmark: DFS on path graph (worst case - linear structure) +static void BM_DFS_PathGraph(benchmark::State& state) { + auto g = create_path_graph(state.range(0)); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_dfs(0)) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_DFS_PathGraph) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: BFS on path graph +static void BM_BFS_PathGraph(benchmark::State& state) { + auto g = create_path_graph(state.range(0)); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_bfs(0)) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_BFS_PathGraph) + ->RangeMultiplier(2) + ->Range(100, 10000) + ->Complexity(); + +// Benchmark: DFS on complete graph (dense) +static void BM_DFS_CompleteGraph(benchmark::State& state) { + auto g = create_complete_graph(state.range(0)); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_dfs(0)) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_DFS_CompleteGraph) + ->RangeMultiplier(2) + ->Range(10, 100) // Smaller range for complete graphs + ->Complexity(); + +// Benchmark: BFS on complete graph (dense) +static void BM_BFS_CompleteGraph(benchmark::State& state) { + auto g = create_complete_graph(state.range(0)); + + for (auto _ : state) { + size_t count = 0; + for (auto [v] : g | vertices_bfs(0)) { + benchmark::DoNotOptimize(v); + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_BFS_CompleteGraph) + ->RangeMultiplier(2) + ->Range(10, 100) // Smaller range for complete graphs + ->Complexity(); diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake index d27f170..337bd9a 100644 --- a/cmake/CPM.cmake +++ b/cmake/CPM.cmake @@ -27,6 +27,15 @@ endif() include(${CPM_DOWNLOAD_LOCATION}) +# Fetch tl::expected for C++20 std::expected polyfill +CPMAddPackage( + NAME expected + GITHUB_REPOSITORY TartanLlama/expected + GIT_TAG v1.1.0 + OPTIONS + "EXPECTED_BUILD_TESTS OFF" +) + # Usage example: # include(CPM) # CPMAddPackage("gh:catchorg/Catch2@3.5.0") diff --git a/docs/view_chaining_limitations.md b/docs/view_chaining_limitations.md new file mode 100644 index 0000000..90bce88 --- /dev/null +++ b/docs/view_chaining_limitations.md @@ -0,0 +1,227 @@ +# View Chaining Limitations with std::views + +## Overview + +Graph views in this library support C++20 range operations and can chain with `std::views` adaptors. However, there is an important limitation when using views with **capturing lambda value functions**. + +## The Problem + +When a view is created with a capturing lambda as a value function, it **cannot be chained** with `std::views` adaptors like `filter`, `transform`, `take`, etc. + +### Example That FAILS + +```cpp +std::vector> g = {{1, 2}, {0, 2}, {0, 1}}; + +// Capturing lambda as value function +auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; + +// This FAILS to compile: +auto view = g | vertexlist(vvf) + | std::views::take(2); // ❌ Compilation error +``` + +### Compilation Error + +``` +error: no match for 'operator|' + (operand types are 'vertexlist_view<..., lambda>' and + 'std::ranges::views::__adaptor::_Partial<...>') +note: constraints not satisfied +note: the required expression '...__is_range_adaptor_closure_fn(...)' is invalid +``` + +## Why This Happens + +The `std::ranges::view` concept requires types to be **semiregular**, which means: +1. **Default constructible**: Can create without arguments +2. **Copyable**: Can be copied +3. **Movable**: Can be moved + +### The Lambda Problem + +Lambdas with captures are **NOT default constructible**: + +```cpp +int data = 42; +auto lambda = [data](int x) { return data + x; }; // Captures 'data' + +// Attempting to default construct this type fails: +decltype(lambda) default_lambda; // ❌ Error: no default constructor +``` + +**Why?** The compiler doesn't know what value to give the captured variable `data` when default-constructing. + +### Impact on Views + +When `vertexlist_view` stores a capturing lambda as `VVF`: +- The view inherits the lambda's properties +- The view becomes **not default_initializable** +- Therefore, the view **doesn't satisfy `std::ranges::view`** +- `std::views` adaptors **refuse to work** with non-view types + +## Concept Check Results + +```cpp +// Lambda with capture +auto lambda = [&g](auto v) { return vertex_id(g, v); }; +using ViewType = vertexlist_view; + +std::ranges::view // ❌ false +std::ranges::range // ✅ true +std::ranges::input_range // ✅ true +std::semiregular // ❌ false - this is the problem! +std::default_initializable // ❌ false - root cause +``` + +## Working Patterns + +### ✅ Pattern 1: No Value Function + std::views::transform (RECOMMENDED) + +Instead of passing a value function to the view, use `std::views::transform`: + +```cpp +auto view = g | vertexlist() // No value function + | std::views::transform([&g](auto vi) { + return vertex_id(g, vi.vertex) * 10; + }) + | std::views::take(2); // ✅ Works perfectly! +``` + +**Why this works**: `vertexlist()` without VVF is fully semiregular, and capturing lambdas work fine **inside** `std::views::transform`. + +### ✅ Pattern 2: Value Functions Without Chaining + +Use value functions when you don't need to chain with `std::views`: + +```cpp +auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; +auto view = g | vertexlist(vvf); // ✅ Works fine + +for (auto [vertex, value] : view) { + // Use vertex and value +} +// Just don't try to chain std::views after this +``` + +### ✅ Pattern 3: Extract to Container First + +Extract view results to a container, then chain `std::views`: + +```cpp +std::vector, void>> vertices; +for (auto vi : g | vertexlist()) { + vertices.push_back(vi); +} + +// Now chain std::views on the vector (capturing allowed) +auto view = vertices + | std::views::transform([&g](auto vi) { return vertex_id(g, vi.vertex); }) + | std::views::filter([](auto id) { return id > 0; }); +``` + +### ✅ Pattern 4: Stateless Lambdas Work + +Lambdas **without** captures are default constructible and work for chaining: + +```cpp +// No captures - this is default constructible +auto vvf = [](auto v) { return 42; }; // Stateless + +auto view = g | vertexlist(vvf) + | std::views::take(2); // ✅ Works! +``` + +## C++26 Solution + +### The Fix: std::copyable_function (P2548) + +C++26 will introduce `std::copyable_function`, a type-erased function wrapper that is **always semiregular**, even when wrapping capturing lambdas. + +```cpp +// C++26 (future) +std::copyable_function vvf = [&g](auto v) { + return vertex_id(g, v) * 10; +}; + +// This wrapper IS default_initializable +std::default_initializable // ✅ true! +``` + +### Future Implementation Strategy + +Once C++26 is available, views could internally wrap VVF in `std::copyable_function`: + +```cpp +template +class vertexlist_view { +private: + G* g_ = nullptr; + + // Wrap VVF in copyable_function for C++26+ + #if __cplusplus >= 202600L + std::copyable_function>(vertex_t)> vvf_; + #else + VVF vvf_; // C++20 - has limitations + #endif +}; +``` + +This would enable full chaining with capturing lambdas while maintaining backward compatibility. + +## Related C++ Proposals + +- **P2548R6**: `std::copyable_function` - Type-erased copyable function wrapper +- **P0792R14**: `function_ref` - Non-owning function reference (C++26) +- **P3312R0**: Relaxed `std::function` requirements for views + +## Summary Table + +| Pattern | Chaining | Captures | Default Init | Works? | +|---------|----------|----------|--------------|--------| +| `vertexlist()` | Yes | In `std::views` | ✅ Yes | ✅ Yes | +| `vertexlist(stateless_lambda)` | Yes | No | ✅ Yes | ✅ Yes | +| `vertexlist(capturing_lambda)` | **No** | Yes | ❌ No | ❌ No | +| `vertexlist(capturing_lambda)` | No | Yes | ❌ No | ✅ Yes | + +## Testing Guidelines + +When writing tests for views: + +1. **Test basic iteration** - All patterns work +2. **Test chaining without value functions** - Always works +3. **Test value functions standalone** - Always works +4. **Don't test chaining with capturing value functions** - Won't compile (this is expected) + +### Example Test Structure + +```cpp +TEST_CASE("vertexlist - basic iteration") { + auto g = make_test_graph(); + auto view = g | vertexlist(); + // Test basic iteration +} + +TEST_CASE("vertexlist - with value function (no chaining)") { + auto g = make_test_graph(); + auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; + auto view = g | vertexlist(vvf); // Don't chain after this + // Test iteration with values +} + +TEST_CASE("vertexlist - chaining with std::views") { + auto g = make_test_graph(); + auto view = g | vertexlist() // No value function + | std::views::transform([&g](auto vi) { + return vertex_id(g, vi.vertex); + }); + // Test chained view +} +``` + +## References + +- [C++20 Ranges](https://en.cppreference.com/w/cpp/ranges) +- [std::ranges::view concept](https://en.cppreference.com/w/cpp/ranges/view) +- [std::semiregular concept](https://en.cppreference.com/w/cpp/concepts/semiregular) +- [P2548R6 - copyable_function](https://wg21.link/p2548r6) diff --git a/docs/views.md b/docs/views.md new file mode 100644 index 0000000..4f622b6 --- /dev/null +++ b/docs/views.md @@ -0,0 +1,810 @@ +# Graph Views Documentation + +## Overview + +Graph views provide lazy, range-based access to graph elements using C++20 ranges and structured bindings. Views are composable, support pipe syntax, and integrate seamlessly with standard library range adaptors. + +**Key Features**: +- Lazy evaluation - elements computed on-demand +- Zero-copy where possible - views reference graph data +- Structured bindings - elegant `auto [v, val]` syntax +- Range adaptor closures - pipe syntax `g | view()` +- Standard library integration - chain with `std::views` +- Descriptor-based - value functions receive descriptors + +## Quick Start + +```cpp +#include +#include + +using namespace graph; +using namespace graph::views::adaptors; + +// Create a graph (vector-of-vectors for simplicity) +std::vector> g(5); +g[0] = {1, 2}; +g[1] = {2, 3}; +g[2] = {3, 4}; +g[3] = {4}; +g[4] = {}; + +// Iterate over all vertices +for (auto [v] : g | vertexlist()) { + std::cout << "Vertex: " << vertex_id(g, v) << "\n"; +} + +// Iterate over edges from vertex 0 +for (auto [e] : g | incidence(0)) { + std::cout << "Edge to: " << target_id(g, e) << "\n"; +} + +// DFS traversal from vertex 0 +for (auto [v] : g | vertices_dfs(0)) { + std::cout << "Visited: " << vertex_id(g, v) << "\n"; +} +``` + +## Basic Views + +### vertexlist + +Iterates over all vertices in the graph. + +**Signature**: +```cpp +auto vertexlist(G&& g); +auto vertexlist(G&& g, VVF&& vvf); // with value function +``` + +**Info Struct**: +```cpp +struct vertex_info { + vertex_descriptor vertex; + // optional: value_type value (if value function provided) +}; +``` + +**Example**: +```cpp +// Without value function +for (auto [v] : g | vertexlist()) { + auto id = vertex_id(g, v); + std::cout << "Vertex " << id << "\n"; +} + +// With value function +auto vvf = [&](auto v) { return vertex_id(g, v) * 2; }; +for (auto [v, val] : g | vertexlist(vvf)) { + std::cout << "Vertex value: " << val << "\n"; +} +``` + +**Use Cases**: +- Iterate all vertices +- Apply operations to every vertex +- Count vertices +- Filter vertices by property + +--- + +### incidence + +Iterates over outgoing edges from a specific vertex. + +**Signature**: +```cpp +auto incidence(G&& g, UID uid); +auto incidence(G&& g, UID uid, EVF&& evf); // with value function +``` + +**Parameters**: +- `uid`: vertex ID or vertex descriptor (seed vertex) +- `evf`: optional edge value function + +**Info Struct**: +```cpp +struct edge_info { + edge_descriptor edge; + // optional: value_type value (if value function provided) +}; +``` + +**Example**: +```cpp +// Without value function +for (auto [e] : g | incidence(0)) { + auto target = target_id(g, e); + std::cout << "Edge to " << target << "\n"; +} + +// With value function +auto evf = [&](auto e) { return target_id(g, e); }; +for (auto [e, target] : g | incidence(0, evf)) { + std::cout << "Target: " << target << "\n"; +} +``` + +**Use Cases**: +- Find neighbors of a vertex +- Calculate out-degree +- Traverse outgoing edges +- Check connectivity + +--- + +### neighbors + +Iterates over adjacent vertices (targets of outgoing edges). + +**Signature**: +```cpp +auto neighbors(G&& g, UID uid); +auto neighbors(G&& g, UID uid, VVF&& vvf); // with value function +``` + +**Parameters**: +- `uid`: vertex ID or vertex descriptor (seed vertex) +- `vvf`: optional vertex value function + +**Info Struct**: +```cpp +struct neighbor_info { + vertex_descriptor vertex; // neighbor vertex descriptor + // optional: value_type value (if value function provided) +}; +``` + +**Example**: +```cpp +// Without value function +for (auto [n] : g | neighbors(0)) { + auto id = vertex_id(g, n); + std::cout << "Neighbor: " << id << "\n"; +} + +// With value function +auto vvf = [&](auto v) { return vertex_id(g, v) * 10; }; +for (auto [n, val] : g | neighbors(0, vvf)) { + std::cout << "Neighbor value: " << val << "\n"; +} +``` + +**Use Cases**: +- Direct neighbor access +- Calculate degree +- Find adjacent vertices +- Build adjacency information + +--- + +### edgelist + +Iterates over all edges in the graph (flattened iteration). + +**Signature**: +```cpp +auto edgelist(G&& g); +auto edgelist(G&& g, EVF&& evf); // with value function +``` + +**Info Struct**: +```cpp +struct edge_info { + edge_descriptor edge; + // optional: value_type value (if value function provided) +}; +``` + +**Example**: +```cpp +// Without value function +for (auto [e] : g | edgelist()) { + auto src = source_id(g, e); + auto tgt = target_id(g, e); + std::cout << "Edge: " << src << " -> " << tgt << "\n"; +} + +// With value function +auto evf = [&](auto e) { + return std::pair{source_id(g, e), target_id(g, e)}; +}; +for (auto [e, endpoints] : g | edgelist(evf)) { + auto [src, tgt] = endpoints; + std::cout << src << " -> " << tgt << "\n"; +} +``` + +**Use Cases**: +- Count total edges +- Iterate all edges +- Build edge list +- Graph transformations + +## Search Views + +Search views perform graph traversal and yield vertices/edges in traversal order. + +### vertices_dfs / edges_dfs + +Depth-first search traversal yielding vertices or edges. + +**Signature**: +```cpp +auto vertices_dfs(G&& g, Seed seed); +auto vertices_dfs(G&& g, Seed seed, VVF&& vvf); +auto vertices_dfs(G&& g, Seed seed, VVF&& vvf, Alloc alloc); + +auto edges_dfs(G&& g, Seed seed); +auto edges_dfs(G&& g, Seed seed, EVF&& evf); +auto edges_dfs(G&& g, Seed seed, EVF&& evf, Alloc alloc); +``` + +**Parameters**: +- `seed`: starting vertex (ID or descriptor) +- `vvf`/`evf`: optional value function +- `alloc`: custom allocator for visited tracker + +**Info Structs**: +```cpp +struct vertex_info { + vertex_descriptor vertex; + // optional: value_type value +}; + +struct edge_info { + edge_descriptor edge; + // optional: value_type value +}; +``` + +**Example**: +```cpp +// DFS from vertex 0 +for (auto [v] : g | vertices_dfs(0)) { + std::cout << "DFS visited: " << vertex_id(g, v) << "\n"; +} + +// DFS edges (tree edges only) +for (auto [e] : g | edges_dfs(0)) { + auto src = source_id(g, e); + auto tgt = target_id(g, e); + std::cout << "Tree edge: " << src << " -> " << tgt << "\n"; +} +``` + +**Use Cases**: +- DFS traversal +- Reachability testing +- Tree construction +- Path finding + +**Search Control**: +```cpp +auto dfs = vertices_dfs(g, 0); +// Cancel search early +dfs.cancel(); // stops iteration +``` + +--- + +### vertices_bfs / edges_bfs + +Breadth-first search traversal yielding vertices or edges. + +**Signature**: +```cpp +auto vertices_bfs(G&& g, Seed seed); +auto vertices_bfs(G&& g, Seed seed, VVF&& vvf); +auto vertices_bfs(G&& g, Seed seed, VVF&& vvf, Alloc alloc); + +auto edges_bfs(G&& g, Seed seed); +auto edges_bfs(G&& g, Seed seed, EVF&& evf); +auto edges_bfs(G&& g, Seed seed, EVF&& evf, Alloc alloc); +``` + +**Example**: +```cpp +// BFS from vertex 0 +for (auto [v] : g | vertices_bfs(0)) { + std::cout << "BFS level order: " << vertex_id(g, v) << "\n"; +} + +// BFS edges (tree edges only) +for (auto [e] : g | edges_bfs(0)) { + std::cout << "BFS tree edge\n"; +} +``` + +**Use Cases**: +- Shortest path (unweighted) +- Level-order traversal +- Distance calculation +- Connected components + +**Search Inspection**: +```cpp +auto bfs = vertices_bfs(g, 0); +// Query search state +auto depth = bfs.depth(); // current depth level +auto count = bfs.size(); // vertices visited so far +``` + +--- + +### vertices_topological_sort / edges_topological_sort + +Topological sort traversal for directed acyclic graphs (DAGs). + +**Signature**: +```cpp +auto vertices_topological_sort(G&& g); +auto vertices_topological_sort(G&& g, VVF&& vvf); +auto vertices_topological_sort(G&& g, VVF&& vvf, Alloc alloc); + +auto edges_topological_sort(G&& g); +auto edges_topological_sort(G&& g, EVF&& evf); +auto edges_topological_sort(G&& g, EVF&& evf, Alloc alloc); +``` + +**Example**: +```cpp +// Topological order +for (auto [v] : g | vertices_topological_sort()) { + std::cout << "Topo order: " << vertex_id(g, v) << "\n"; +} + +// Edges in topological order +for (auto [e] : g | edges_topological_sort()) { + // Process edges in dependency order +} +``` + +**Use Cases**: +- Task scheduling +- Build systems +- Dependency resolution +- DAG processing + +**Cycle Detection**: +```cpp +// Safe version with cycle detection +try { + for (auto [v] : g | vertices_topological_sort()) { + // Process vertices + } +} catch (const graph::views::cycle_detected& e) { + std::cerr << "Graph has cycles!\n"; +} +``` + +## Range Adaptor Syntax + +All views support pipe operator for elegant chaining: + +```cpp +// Pipe syntax (recommended) +auto view = g | vertexlist(); +auto view = g | incidence(0); +auto view = g | vertices_dfs(0); + +// Direct call syntax (also supported) +auto view = vertexlist(g); +auto view = incidence(g, 0); +auto view = vertices_dfs(g, 0); +``` + +## Value Functions + +Value functions receive descriptors and compute additional values: + +**Vertex Value Function**: +```cpp +auto vvf = [&g](auto v) { + return vertex_id(g, v) * 2; +}; +for (auto [v, val] : g | vertexlist(vvf)) { + // val = vertex_id * 2 +} +``` + +**Edge Value Function**: +```cpp +auto evf = [&g](auto e) { + return target_id(g, e); +}; +for (auto [e, target] : g | incidence(0, evf)) { + // target = target vertex ID +} +``` + +**Complex Value Functions**: +```cpp +// Return struct +struct VertexData { + size_t id; + size_t degree; +}; + +auto vvf = [&g](auto v) -> VertexData { + return {vertex_id(g, v), degree(g, v)}; +}; + +for (auto [v, data] : g | vertexlist(vvf)) { + std::cout << "Vertex " << data.id + << " has degree " << data.degree << "\n"; +} +``` + +## Chaining with std::views + +Graph views integrate with C++20 standard ranges: + +```cpp +#include + +// Filter vertices +auto even_vertices = g + | vertexlist() + | std::views::filter([&g](auto info) { + auto [v] = info; + return vertex_id(g, v) % 2 == 0; + }); + +// Transform values +auto doubled = g + | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v) * 2; + }); + +// Take first N vertices +auto first_five = g + | vertexlist() + | std::views::take(5); + +// Complex chains +auto result = g + | vertices_dfs(0) + | std::views::drop(1) // skip first + | std::views::take(10) // take next 10 + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }); + +for (auto id : result) { + std::cout << id << "\n"; +} +``` + +## Performance Considerations + +### Lazy Evaluation + +Views compute elements on-demand: + +```cpp +// No computation until iteration +auto view = g | vertexlist(); // O(1) + +// Computation happens during iteration +for (auto [v] : view) { // O(n) total + // Process vertex +} +``` + +### Zero-Copy Design + +Basic views reference graph data without copying: + +```cpp +// vertexlist, incidence, neighbors are zero-copy +auto view = g | vertexlist(); // No allocation + +// Search views maintain internal state +auto dfs = g | vertices_dfs(0); // Allocates visited tracker +``` + +### Memory Usage + +| View | Memory | Notes | +|------|--------|-------| +| vertexlist | O(1) | References graph | +| incidence | O(1) | References graph | +| neighbors | O(1) | References graph | +| edgelist | O(1) | References graph | +| vertices_dfs | O(V) | Visited tracker | +| edges_dfs | O(V) | Visited tracker | +| vertices_bfs | O(V) | Visited + queue | +| edges_bfs | O(V) | Visited + queue | +| vertices_topological_sort | O(V) | In-degree map | +| edges_topological_sort | O(V) | In-degree map | + +### Optimization Tips + +**1. Reuse Value Functions**: +```cpp +// Good: define once, reuse +auto vvf = [&g](auto v) { return vertex_id(g, v); }; +auto view1 = g | vertexlist(vvf); +auto view2 = g | vertices_dfs(0, vvf); + +// Avoid: repeated lambda definitions +``` + +**2. Use Structured Bindings**: +```cpp +// Good: structured binding (compiler optimizes) +for (auto [v, val] : g | vertexlist(vvf)) { } + +// Avoid: manual extraction +for (auto info : g | vertexlist(vvf)) { + auto v = info.vertex; + auto val = info.value; +} +``` + +**3. Early Termination**: +```cpp +// Stop iteration when done +for (auto [v] : g | vertices_dfs(0)) { + if (vertex_id(g, v) == target) { + break; // efficient early exit + } +} +``` + +**4. Custom Allocators**: +```cpp +// Use custom allocator for search views +std::pmr::monotonic_buffer_resource pool; +std::pmr::polymorphic_allocator alloc(&pool); + +auto dfs = vertices_dfs(g, 0, void{}, alloc); +// Faster allocation for visited tracker +``` + +## Best Practices + +### 1. Include Headers + +Always include both headers: +```cpp +#include // Core types and CPOs +#include // Views and adaptors +``` + +### 2. Use Pipe Syntax + +Pipe syntax is more readable: +```cpp +// Preferred +auto view = g | vertexlist(); + +// Also valid +auto view = vertexlist(g); +``` + +### 3. Structured Bindings + +Use structured bindings for clarity: +```cpp +// Good +for (auto [v, val] : g | vertexlist(vvf)) { + // Use v and val directly +} + +// Avoid +for (auto info : g | vertexlist(vvf)) { + auto v = info.vertex; + auto val = info.value; +} +``` + +### 4. Const Correctness + +Views work with const graphs: +```cpp +void process(const Graph& g) { + for (auto [v] : g | vertexlist()) { + // Read-only access + } +} +``` + +### 5. Value Function Patterns + +**Pattern 1: Simple Transformation** +```cpp +auto vvf = [&g](auto v) { return vertex_id(g, v); }; +``` + +**Pattern 2: Multiple Values** +```cpp +auto vvf = [&g](auto v) { + return std::tuple{vertex_id(g, v), degree(g, v)}; +}; +for (auto [v, id, deg] : g | vertexlist(vvf)) { } +``` + +**Pattern 3: Computed Properties** +```cpp +auto vvf = [&g](auto v) { + size_t id = vertex_id(g, v); + size_t deg = degree(g, v); + return id * 100 + deg; // composite value +}; +``` + +### 6. Error Handling + +**Cycle Detection**: +```cpp +try { + for (auto [v] : g | vertices_topological_sort()) { + // Process DAG + } +} catch (const graph::views::cycle_detected&) { + // Handle cyclic graph +} +``` + +**Search Cancellation**: +```cpp +auto dfs = vertices_dfs(g, 0); +for (auto [v] : dfs) { + if (should_stop(v)) { + dfs.cancel(); // graceful termination + break; + } +} +``` + +## Common Patterns + +### Pattern 1: Neighbor Iteration +```cpp +// Find all neighbors of a vertex +std::vector get_neighbors(const auto& g, size_t vid) { + std::vector result; + for (auto [n] : g | neighbors(vid)) { + result.push_back(vertex_id(g, n)); + } + return result; +} +``` + +### Pattern 2: Reachability Test +```cpp +// Check if target reachable from source +bool is_reachable(const auto& g, size_t src, size_t tgt) { + for (auto [v] : g | vertices_dfs(src)) { + if (vertex_id(g, v) == tgt) { + return true; + } + } + return false; +} +``` + +### Pattern 3: Shortest Path (BFS) +```cpp +// Find shortest path length (unweighted) +std::optional shortest_path_length( + const auto& g, size_t src, size_t tgt) { + + auto bfs = vertices_bfs(g, src); + for (auto [v] : bfs) { + if (vertex_id(g, v) == tgt) { + return bfs.depth(); + } + } + return std::nullopt; +} +``` + +### Pattern 4: Topological Ordering +```cpp +// Get topological order as vector +std::vector topological_order(const auto& g) { + std::vector result; + for (auto [v] : g | vertices_topological_sort()) { + result.push_back(vertex_id(g, v)); + } + return result; +} +``` + +### Pattern 5: Degree Distribution +```cpp +// Calculate degree distribution +std::unordered_map degree_distribution(const auto& g) { + std::unordered_map dist; + + for (auto [v] : g | vertexlist()) { + size_t deg = 0; + for (auto [e] : g | incidence(vertex_id(g, v))) { + ++deg; + } + ++dist[deg]; + } + return dist; +} +``` + +### Pattern 6: Filtered Iteration +```cpp +// Process only high-degree vertices +for (auto [v] : g | vertexlist()) { + size_t deg = 0; + for (auto [e] : g | incidence(vertex_id(g, v))) { + ++deg; + } + + if (deg > threshold) { + // Process high-degree vertex + } +} +``` + +## Limitations + +### 1. Single-Pass Iteration + +Views are input ranges (single-pass): +```cpp +auto view = g | vertexlist(); + +// First iteration OK +for (auto [v] : view) { } + +// Second iteration: behavior undefined +// Create new view instead: +auto view2 = g | vertexlist(); +for (auto [v] : view2) { } +``` + +### 2. Descriptor Lifetime + +Descriptors valid only during iteration: +```cpp +// Bad: storing descriptor +std::vector> vertices; +for (auto [v] : g | vertexlist()) { + vertices.push_back(v); // descriptor may be invalidated +} + +// Good: store vertex IDs +std::vector vertex_ids; +for (auto [v] : g | vertexlist()) { + vertex_ids.push_back(vertex_id(g, v)); +} +``` + +### 3. Graph Mutation + +Don't modify graph during iteration: +```cpp +// Bad: modifying during iteration +for (auto [v] : g | vertexlist()) { + g.add_vertex(); // undefined behavior! +} + +// Good: collect then modify +std::vector to_process; +for (auto [v] : g | vertexlist()) { + to_process.push_back(vertex_id(g, v)); +} +// Now safe to modify graph +for (auto id : to_process) { + g.add_edge(id, ...); +} +``` + +## See Also + +- [CPO Documentation](cpo.md) - Customization point objects +- [Graph Concepts](common_graph_guidelines.md) - Graph interface requirements +- [View Chaining Limitations](view_chaining_limitations.md) - Advanced chaining patterns diff --git a/include/graph/adj_list/adjacency_list_concepts.hpp b/include/graph/adj_list/adjacency_list_concepts.hpp index 6b81789..4eb07eb 100644 --- a/include/graph/adj_list/adjacency_list_concepts.hpp +++ b/include/graph/adj_list/adjacency_list_concepts.hpp @@ -186,13 +186,16 @@ concept vertex_range = /** * @brief Concept for a graph with random access range of vertices * - * An index_vertex_range is a vertex_range where vertices additionally support - * random access, allowing O(1) access to any vertex by index. + * An index_vertex_range is a vertex_range where the underlying vertex container + * supports random access, allowing O(1) access to any vertex by index. * * Requirements: * - Must satisfy vertex_range - * - vertices(g) must return a std::ranges::random_access_range - * - Supports operator[] or equivalent for O(1) access + * - The underlying iterator of the vertex_descriptor_view must be a random_access_iterator + * + * Note: We check the underlying iterator type, not the view itself, because + * vertex_descriptor_view is always a forward_range (synthesizes descriptors on-the-fly) + * but the underlying container may still support random access. * * Examples: * - vertex_descriptor_view over std::vector (index-based) @@ -206,7 +209,7 @@ concept vertex_range = template concept index_vertex_range = vertex_range && - std::ranges::random_access_range>; + std::random_access_iterator::vertex_desc::iterator_type>; // ============================================================================= // Adjacency List Concepts diff --git a/include/graph/adj_list/detail/graph_cpo.hpp b/include/graph/adj_list/detail/graph_cpo.hpp index ccdb493..490ee0d 100644 --- a/include/graph/adj_list/detail/graph_cpo.hpp +++ b/include/graph/adj_list/detail/graph_cpo.hpp @@ -20,6 +20,7 @@ #include "graph/adj_list/edge_descriptor_view.hpp" #include "graph/adj_list/descriptor.hpp" #include "graph/adj_list/descriptor_traits.hpp" +#include "graph/edge_list/edge_list_traits.hpp" namespace graph::adj_list { @@ -881,7 +882,15 @@ namespace _cpo_impls { // ========================================================================= namespace _target_id { - enum class _St { _none, _native_edge_member, _adl_descriptor, _descriptor }; + enum class _St { + _none, + _native_edge_member, + _adl_descriptor, + _adj_list_descriptor, // Tier 4: adj_list::edge_descriptor (renamed from _descriptor) + _edge_list_descriptor, // Tier 5: edge_list::edge_descriptor + _edge_info_member, // Tier 6: edge_info data member + _tuple_like // Tier 7: tuple/pair + }; // Check if the underlying native edge type has target_id() member - highest priority // This checks uv.value()->target_id() where value() returns iterator to native edge @@ -900,17 +909,52 @@ namespace _cpo_impls { { target_id(g, uv) }; }; - // Check if descriptor has target_id() member (default lowest priority) + // Check if adj_list descriptor has target_id() member (Tier 4) // Note: edge_descriptor.target_id() requires the edge container, not the graph // underlying_value() gives us the vertex for vov or the edge container for raw adjacency lists // The edge_descriptor.target_id() handles extracting the target ID correctly in both cases template - concept _has_descriptor = is_edge_descriptor_v> && + concept _has_adj_list_descriptor = is_edge_descriptor_v> && requires(G& g, const E& uv) { { uv.source().underlying_value(g) }; { uv.target_id(uv.source().underlying_value(g)) }; }; + // Tier 5: Check if edge_list descriptor has target_id() member + template + concept _has_edge_list_descriptor = edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { uv.target_id() }; + }; + + // Tier 6: Check for edge_info-style direct data member access + // Must NOT be a descriptor type (to avoid ambiguity with method calls) + template + concept _has_edge_info_member = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + uv.target_id; // data member, not method + } && + !requires(const UV& uv) { + uv.target_id(); // exclude if it's callable (i.e., a method) + }; + + // Tier 7: Check for tuple-like edge (pair, tuple) + // Must NOT be any descriptor type or have edge_info members + template + concept _is_tuple_like_edge = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + !_has_edge_info_member && + requires { + std::tuple_size>::value; + } && + requires(const UV& uv) { + { std::get<0>(uv) }; + { std::get<1>(uv) }; + }; + template [[nodiscard]] consteval _Choice_t<_St> _Choose() noexcept { if constexpr (_has_native_edge_member) { @@ -919,9 +963,18 @@ namespace _cpo_impls { } else if constexpr (_has_adl_descriptor) { return {_St::_adl_descriptor, noexcept(target_id(std::declval(), std::declval()))}; - } else if constexpr (_has_descriptor) { + } else if constexpr (_has_adj_list_descriptor) { // Default to false (safe) since we have conditional logic - return {_St::_descriptor, false}; + return {_St::_adj_list_descriptor, false}; + } else if constexpr (_has_edge_list_descriptor) { + return {_St::_edge_list_descriptor, + noexcept(std::declval().target_id())}; + } else if constexpr (_has_edge_info_member) { + return {_St::_edge_info_member, + noexcept(std::declval().target_id)}; + } else if constexpr (_is_tuple_like_edge) { + return {_St::_tuple_like, + noexcept(std::get<1>(std::declval()))}; } else { return {_St::_none, false}; } @@ -936,29 +989,39 @@ namespace _cpo_impls { /** * @brief Get target vertex ID from an edge * - * Resolution order (three-tier approach): + * Resolution order (seven-tier approach): * 1. (*uv.value()).target_id() - Native edge member function (highest priority) - * 2. target_id(g, uv) - ADL with edge_descriptor (medium priority) - * 3. uv.target_id(uv.source().inner_value(g)) - descriptor's default method (lowest priority) + * 2. target_id(g, uv) - ADL with edge_descriptor + * 3. uv.target_id(uv.source().inner_value(g)) - adj_list::edge_descriptor (Tier 4) + * 4. uv.target_id() - edge_list::edge_descriptor member (Tier 5) + * 5. uv.target_id - edge_info data member (Tier 6) + * 6. std::get<1>(uv) - tuple-like edge (Tier 7, lowest priority) * * Where: - * - uv must be edge_t (the edge descriptor type for graph G) + * - uv must be edge_t (the edge descriptor type for graph G) for tiers 1-4 + * - For tiers 5-7, uv can be edge_list descriptors, edge_info structs, or tuple-like types * - The native edge member function is called if the underlying edge type has target_id() - * - The default implementation uses the edge container from the source vertex + * - ADL allows customization by providing a free function * - * Edge data extraction (default implementation): + * Tiers 4-7 support: + * - adj_list edge descriptors (existing adjacency list edges) + * - edge_list descriptors (new edge list support) + * - edge_info structs with direct data members + * - tuple/pair representations (source, target, [value]) + * + * Edge data extraction (tier 4 default implementation): * - Simple integral type (int): Returns the value itself (the target ID) * - Pair: Returns .first (the target ID) * - Tuple: Returns std::get<0> (the target ID) * - Custom struct/type: User provides custom extraction via member function or ADL * * @tparam G Graph type - * @tparam E Edge descriptor type (constrained to be an edge_descriptor_type) + * @tparam E Edge descriptor or edge type * @param g Graph container - * @param uv Edge descriptor (must be edge_t - the edge descriptor type for the graph) + * @param uv Edge descriptor or edge * @return Target vertex identifier */ - template + template [[nodiscard]] constexpr auto operator()(G& g, const E& uv) const noexcept(_Choice, std::remove_cvref_t>._No_throw) -> decltype(auto) @@ -972,11 +1035,17 @@ namespace _cpo_impls { return (*uv.value()).target_id(); } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adl_descriptor) { return target_id(g, uv); - } else if constexpr (_Choice<_G, _E>._Strategy == _St::_descriptor) { + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adj_list_descriptor) { // Default: use edge_descriptor.target_id() with vertex from underlying_value // For vov: underlying_value gives vertex, edge_descriptor extracts from it // For raw adjacency lists: underlying_value gives edge container directly return uv.target_id(uv.source().underlying_value(g)); + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_list_descriptor) { + return uv.target_id(); + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_info_member) { + return uv.target_id; + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_tuple_like) { + return std::get<1>(uv); } } }; @@ -2427,7 +2496,16 @@ namespace _cpo_impls { // ========================================================================= namespace _edge_value { - enum class _St { _none, _member, _adl, _value_fn, _default }; + enum class _St { + _none, + _member, + _adl, + _value_fn, + _adj_list_descriptor, // Tier 4: adj_list::edge_descriptor (renamed from _default) + _edge_list_descriptor, // Tier 5: edge_list::edge_descriptor + _edge_info_member, // Tier 6: edge_info data member + _tuple_like // Tier 7: tuple (3+ elements) + }; // Check for g.edge_value(uv) member function // Note: Uses G (not G&) to preserve const qualification @@ -2452,18 +2530,56 @@ namespace _cpo_impls { { uv.value() }; }; - // Check if we can use default: uv.inner_value(v) where v = uv.source().underlying_value(g) + // Check if we can use adj_list descriptor default: uv.inner_value(v) where v = uv.source().underlying_value(g) // Note: Uses G (not G&) to preserve const qualification // underlying_value() gives us the vertex for vov or the edge container for raw adjacency lists // The edge_descriptor.inner_value() handles extracting properties correctly in both cases template - concept _has_default = + concept _has_adj_list_descriptor = is_edge_descriptor_v> && requires(G g, const E& uv) { { uv.source().underlying_value(g) }; { uv.inner_value(uv.source().underlying_value(g)) }; }; + // Tier 5: Check if edge_list descriptor has value() member + template + concept _has_edge_list_descriptor = edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { uv.value() }; + }; + + // Tier 6: Check for edge_info-style direct data member access + // Must NOT be a descriptor type (to avoid ambiguity with method calls) + template + concept _has_edge_info_member = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + uv.value; // data member, not method + } && + !requires(const UV& uv) { + uv.value(); // exclude if it's callable (i.e., a method) + }; + + // Tier 7: Check for tuple-like edge (pair, tuple) + // Must NOT be any descriptor type or have edge_info members + // Note: For edge values, this would be std::get<2> (third element) + template + concept _is_tuple_like_edge = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + !_has_edge_info_member && + requires { + std::tuple_size>::value; + requires std::tuple_size>::value >= 3; + } && + requires(const UV& uv) { + { std::get<0>(uv) }; + { std::get<1>(uv) }; + { std::get<2>(uv) }; // edge value is third element + }; + template [[nodiscard]] consteval _Choice_t<_St> _Choose() noexcept { if constexpr (_has_member) { @@ -2475,9 +2591,18 @@ namespace _cpo_impls { } else if constexpr (_has_value_fn) { return {_St::_value_fn, noexcept(std::declval().value())}; - } else if constexpr (_has_default) { + } else if constexpr (_has_adj_list_descriptor) { // Default to false (safe) since we have conditional logic - return {_St::_default, false}; + return {_St::_adj_list_descriptor, false}; + } else if constexpr (_has_edge_list_descriptor) { + return {_St::_edge_list_descriptor, + noexcept(std::declval().value())}; + } else if constexpr (_has_edge_info_member) { + return {_St::_edge_info_member, + noexcept(std::declval().value)}; + } else if constexpr (_is_tuple_like_edge) { + return {_St::_tuple_like, + noexcept(std::get<2>(std::declval()))}; } else { return {_St::_none, false}; } @@ -2492,13 +2617,26 @@ namespace _cpo_impls { /** * @brief Get the user-defined value associated with an edge * - * Resolution order: + * Resolution order (seven-tier approach): * 1. g.edge_value(uv) - Member function (highest priority) - * 2. edge_value(g, uv) - ADL (high priority) - * 3. uv.value() - Member function on edge (medium priority) - * 4. uv.inner_value(edges) - Default using edge descriptor's inner_value (lowest priority) + * 2. edge_value(g, uv) - ADL + * 3. uv.value() - Member function on edge + * 4. uv.inner_value(edges) - adj_list::edge_descriptor (Tier 4) + * 5. uv.value() - edge_list::edge_descriptor member (Tier 5) + * 6. uv.value - edge_info data member (Tier 6) + * 7. std::get<2>(uv) - tuple-like edge (Tier 7, lowest priority) * - * The default implementation: + * Where: + * - uv must be edge_t (the edge descriptor type for graph G) for tiers 1-4 + * - For tiers 5-7, uv can be edge_list descriptors, edge_info structs, or tuple-like types + * + * Tiers 4-7 support: + * - adj_list edge descriptors (existing adjacency list edges) + * - edge_list descriptors (new edge list support) + * - edge_info structs with direct data members + * - tuple/triple representations (source, target, value) + * + * The tier 4 default implementation: * - Uses uv.inner_value(edges) where edges = uv.source().inner_value(g) * - For simple edges (int): returns the value itself (the target ID) * - For pair edges (target, weight): returns .second (the weight/property) @@ -2508,12 +2646,12 @@ namespace _cpo_impls { * This provides access to user-defined edge properties/weights stored in the graph. * * @tparam G Graph type - * @tparam E Edge descriptor type (constrained to be an edge_descriptor_type) + * @tparam E Edge descriptor or edge type * @param g Graph container - * @param uv Edge descriptor + * @param uv Edge descriptor or edge * @return Reference to the edge value/properties (or by-value if custom implementation returns by-value) */ - template + template [[nodiscard]] constexpr decltype(auto) operator()(G&& g, E&& uv) const noexcept(_Choice, std::remove_cvref_t>._No_throw) requires (_Choice, std::remove_cvref_t>._Strategy != _St::_none) @@ -2527,13 +2665,19 @@ namespace _cpo_impls { return edge_value(g, std::forward(uv)); } else if constexpr (_Choice<_G, _E>._Strategy == _St::_value_fn) { return std::forward(uv).value(); - } else if constexpr (_Choice<_G, _E>._Strategy == _St::_default) { + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adj_list_descriptor) { // Get vertex from underlying_value - works for both vov and raw adjacency lists // For vov: gives vertex, edge_descriptor extracts properties from it // For raw adjacency lists: gives edge container directly return std::forward(uv).inner_value( std::forward(uv).source().underlying_value(std::forward(g)) ); + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_list_descriptor) { + return uv.value(); + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_info_member) { + return uv.value; + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_tuple_like) { + return std::get<2>(uv); } } }; @@ -2657,7 +2801,16 @@ namespace _cpo_impls { // ========================================================================= namespace _source_id { - enum class _St { _none, _native_edge_member, _member, _adl, _descriptor }; + enum class _St { + _none, + _native_edge_member, + _member, + _adl, + _adj_list_descriptor, // Tier 4: adj_list::edge_descriptor (renamed from _descriptor) + _edge_list_descriptor, // Tier 5: edge_list::edge_descriptor + _edge_info_member, // Tier 6: edge_info data member + _tuple_like // Tier 7: tuple/pair + }; // Check if the underlying native edge type has source_id() member - highest priority // This checks uv.value()->source_id() where value() returns iterator to native edge @@ -2681,13 +2834,48 @@ namespace _cpo_impls { { source_id(g, uv) }; }; - // Check if edge descriptor has source_id() member (default) + // Check if adj_list edge descriptor has source_id() member (Tier 4) template - concept _has_descriptor = is_edge_descriptor_v> && + concept _has_adj_list_descriptor = is_edge_descriptor_v> && requires(const E& uv) { { uv.source_id() }; }; + // Tier 5: Check if edge_list descriptor has source_id() member + template + concept _has_edge_list_descriptor = edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + { uv.source_id() }; + }; + + // Tier 6: Check for edge_info-style direct data member access + // Must NOT be a descriptor type (to avoid ambiguity with method calls) + template + concept _has_edge_info_member = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + requires(const UV& uv) { + uv.source_id; // data member, not method + } && + !requires(const UV& uv) { + uv.source_id(); // exclude if it's callable (i.e., a method) + }; + + // Tier 7: Check for tuple-like edge (pair, tuple) + // Must NOT be any descriptor type or have edge_info members + template + concept _is_tuple_like_edge = + !is_edge_descriptor_v> && + !edge_list::is_edge_list_descriptor_v> && + !_has_edge_info_member && + requires { + std::tuple_size>::value; + } && + requires(const UV& uv) { + { std::get<0>(uv) }; + { std::get<1>(uv) }; + }; + template [[nodiscard]] consteval _Choice_t<_St> _Choose() noexcept { if constexpr (_has_native_edge_member) { @@ -2699,9 +2887,18 @@ namespace _cpo_impls { } else if constexpr (_has_adl) { return {_St::_adl, noexcept(source_id(std::declval(), std::declval()))}; - } else if constexpr (_has_descriptor) { - return {_St::_descriptor, + } else if constexpr (_has_adj_list_descriptor) { + return {_St::_adj_list_descriptor, + noexcept(std::declval().source_id())}; + } else if constexpr (_has_edge_list_descriptor) { + return {_St::_edge_list_descriptor, noexcept(std::declval().source_id())}; + } else if constexpr (_has_edge_info_member) { + return {_St::_edge_info_member, + noexcept(std::declval().source_id)}; + } else if constexpr (_is_tuple_like_edge) { + return {_St::_tuple_like, + noexcept(std::get<0>(std::declval()))}; } else { return {_St::_none, false}; } @@ -2716,23 +2913,26 @@ namespace _cpo_impls { /** * @brief Get the source vertex ID for an edge * - * Resolution order (four-tier approach): + * Resolution order (seven-tier approach): * 1. (*uv.value()).source_id() - Native edge member function (highest priority) - * 2. g.source_id(uv) - Graph member function (high priority) - * 3. source_id(g, uv) - ADL (medium priority) - * 4. uv.source_id() - Edge descriptor's source_id() member (lowest priority) + * 2. g.source_id(uv) - Graph member function + * 3. source_id(g, uv) - ADL + * 4. uv.source_id() - adj_list::edge_descriptor member (Tier 4) + * 5. uv.source_id() - edge_list::edge_descriptor member (Tier 5) + * 6. uv.source_id - edge_info data member (Tier 6) + * 7. std::get<0>(uv) - tuple-like edge (Tier 7, lowest priority) * * Where: * - uv must be edge_t (the edge descriptor type for graph G) * - The native edge member function is called if the underlying edge type has source_id() * - ADL allows customization by providing a free function that takes the descriptor + * - Tier 4-7 provide fallback implementations for various edge representations * - * The default implementation (tier 4) works for any edge_descriptor that - * stores its source vertex. This is available for standard adjacency list - * graphs where edge descriptors maintain their source vertex reference. - * - * For specialized graph types (bidirectional graphs, edge lists), provide - * a custom member function or ADL override. + * Tiers 4-7 support: + * - adj_list edge descriptors (existing adjacency list edges) + * - edge_list descriptors (new edge list support) + * - edge_info structs with direct data members + * - tuple/pair representations (source, target, [value]) * * @tparam G Graph type * @tparam E Edge descriptor or edge type @@ -2754,8 +2954,14 @@ namespace _cpo_impls { return g.source_id(uv); } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adl) { return source_id(g, uv); - } else if constexpr (_Choice<_G, _E>._Strategy == _St::_descriptor) { + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adj_list_descriptor) { + return uv.source_id(); + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_list_descriptor) { return uv.source_id(); + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_info_member) { + return uv.source_id; + } else if constexpr (_Choice<_G, _E>._Strategy == _St::_tuple_like) { + return std::get<0>(uv); } } }; diff --git a/include/graph/container/undirected_adjacency_list.hpp b/include/graph/container/undirected_adjacency_list.hpp index 88df32d..734df69 100644 --- a/include/graph/container/undirected_adjacency_list.hpp +++ b/include/graph/container/undirected_adjacency_list.hpp @@ -1234,6 +1234,23 @@ constexpr VId source_id([[maybe_unused]] const undirected_adjacency_list(e.source_id()); } +/// edge_value(g, edge_descriptor) - get edge value from edge descriptor +/// Extracts the edge value from the underlying edge pointed to by the descriptor. +/// Only enabled when EV is not void. +template class VContainer, typename Alloc, typename E> + requires edge_descriptor_type && (!std::is_void_v) +constexpr decltype(auto) edge_value(undirected_adjacency_list&, const E& e) noexcept { + return e.value()->value(); +} + +template class VContainer, typename Alloc, typename E> + requires edge_descriptor_type && (!std::is_void_v) +constexpr decltype(auto) edge_value(const undirected_adjacency_list&, const E& e) noexcept { + return e.value()->value(); +} + ///------------------------------------------------------------------------------------- /// ual_vertex /// diff --git a/include/graph/edge_list/edge_list.hpp b/include/graph/edge_list/edge_list.hpp new file mode 100755 index 0000000..0786370 --- /dev/null +++ b/include/graph/edge_list/edge_list.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include "../graph.hpp" +#include "../adj_list/detail/graph_cpo.hpp" +#include + +#ifndef EDGELIST_HPP +# define EDGELIST_HPP + +// This file implements the interface for an edgelist (el). +// +// An edgelist is a range of edges where source_id(e) and target_id(e) are property +// functions that can be called on an edge (value type of the range). +// +// An optional edge_value(e) property can also be used if a value is defined for +// the edgelist. Use the has_edge_value concept to determine if it defined. +// +// The concepts, types and property functions mirror definitions for edges and +// and edge for an adjacency list. +// + +// edgelist concepts +// ----------------------------------------------------------------------------- +// basic_sourced_edgelist +// basic_sourced_index_edgelist +// has_edge_value +// +// Type aliases +// ----------------------------------------------------------------------------- +// edge_range_t = EL +// edge_iterator_t = range_iterator_t +// edge_t = range_value_t +// edge_reference_t = range_value_t +// edge_value_t = decltype(edge_value(e)) (optional) +// vertex_id_t = decltype(source_id(e)) +// +// edgelist functions +// ----------------------------------------------------------------------------- +// num_edges(el) (todo) +// has_edge(el) (todo) +// contains_edge(el,uid,vid) (todo) +// +// edge functions +// ----------------------------------------------------------------------------- +// source_id(e) +// target_id(e) +// edge_value(e) +// +// Edge definitions supported without overrides +// ----------------------------------------------------------------------------- +// The standard implementation supports two edge types with support for +// source_id(e) and target_id(e) to return their respective values, and an +// optional edge_value(e) if the edge has a value (shown following). The +// functions can be overridden for user-defined edge types. +// +// pair +// tuple +// tuple +// +// edge_info : {source_id, target_id} +// edge_info : {source_id, target_id, EV} +// +// edge_info : {source_id, target_id, edge} +// edge_info : {source_id, target_id, edge, EV} +// +// Naming conventions +// ----------------------------------------------------------------------------- +// Type Variable Description +// -------- ----------- -------------------------------------------------------- +// EL el EdgeList +// E e Edge on an edgelist +// EV val Edge Value +// + +// merge implementation into graph with single namespace? +// Issues: +// 1. name conflict with edgelist view? No: basic_sourced_edgelist vs. views::edgelist. +// 2. template aliases can't be distinguished by concepts +// 3. vertex_id_t definition for adjlist and edgelist have be done in separate locations + +namespace graph { + +// New namespace name (underscore instead of no separator) +namespace edge_list { + // + // edgelist concepts + // + // basic_sourced_edgelist: Supports ANY vertex ID type (int, string, custom types, etc.) + template // For exposition only + concept basic_sourced_edgelist = std::ranges::input_range && // + !std::ranges::range> && // distinguish from adjacency list + requires(EL& el, std::ranges::range_value_t uv) { + { graph::adj_list::_cpo_instances::source_id(el, uv) }; + { graph::adj_list::_cpo_instances::target_id(el, uv) } -> std::convertible_to; + }; + + // basic_sourced_index_edgelist: Requires INTEGRAL vertex IDs (int, size_t, etc.) + template // For exposition only + concept basic_sourced_index_edgelist = basic_sourced_edgelist && // + std::integral(), std::declval>()))>> && + std::integral(), std::declval>()))>>; + + + // (non-basic concepts imply inclusion of an edge reference which doesn't make much sense) + + template // For exposition only + concept has_edge_value = basic_sourced_edgelist && // + requires(EL& el, std::ranges::range_value_t uv) { + { graph::adj_list::_cpo_instances::edge_value(el, uv) }; + }; + + template + struct is_directed : public false_type {}; // specialized for graph container + + template + inline constexpr bool is_directed_v = is_directed::value; + + + // + // edgelist types (note that concepts don't really do anything except document expectations) + // + template // For exposition only + using edge_range_t = EL; + + template // For exposition only + using edge_iterator_t = std::ranges::iterator_t>; + + template // For exposition only + using edge_t = std::ranges::range_value_t>; + + template // For exposition only + using edge_reference_t = std::ranges::range_reference_t>; + + template // For exposition only + using edge_value_t = std::remove_cvref_t&>(), + std::declval>>()))>; + + template // For exposition only + using vertex_id_t = std::remove_cvref_t&>(), + std::declval>>()))>; + + + // template aliases can't be distinguished with concepts :( + // + //template // For exposition only + //using vid_t = decltype(vertex_id(declval())); + // + //template // For exposition only + //using vid_t = decltype(source_id(declval>())); + +} // namespace edge_list + +} // namespace graph + +#endif diff --git a/include/graph/edge_list/edge_list_descriptor.hpp b/include/graph/edge_list/edge_list_descriptor.hpp new file mode 100644 index 0000000..0556349 --- /dev/null +++ b/include/graph/edge_list/edge_list_descriptor.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include + +namespace graph::edge_list { + +namespace detail { + // Empty type for void value optimization + struct empty_value { + constexpr auto operator<=>(const empty_value&) const noexcept = default; + }; +} + +/** + * @brief Lightweight edge descriptor for edge lists + * + * This descriptor is a non-owning reference to edge data stored in an edge list. + * It stores references to the source ID, target ID, and optionally the edge value, + * avoiding any copies. The descriptor is only valid as long as the referenced data exists. + * + * @tparam VId Vertex ID type + * @tparam EV Edge value type (void for edges without values) + */ +template +class edge_descriptor { +public: + using vertex_id_type = VId; + using edge_value_type = EV; + + // Constructor without value (for void EV) + constexpr edge_descriptor(const VId& src, const VId& tgt) + requires std::is_void_v + : source_id_(src), target_id_(tgt), value_() {} + + // Constructor with value (for non-void EV) + template + requires (!std::is_void_v) + constexpr edge_descriptor(const VId& src, const VId& tgt, const E& val) + : source_id_(src), target_id_(tgt), value_(std::cref(val)) {} + + // Copy constructor and assignment - this is a reference type + edge_descriptor(const edge_descriptor&) = default; + edge_descriptor& operator=(const edge_descriptor&) = delete; + + // Move constructor and assignment - this is a reference type + edge_descriptor(edge_descriptor&&) = default; + edge_descriptor& operator=(edge_descriptor&&) = delete; + + // Accessors - return the stored references + [[nodiscard]] constexpr const VId& source_id() const noexcept { + return source_id_; + } + + [[nodiscard]] constexpr const VId& target_id() const noexcept { + return target_id_; + } + + // Value accessor (only for non-void EV) + template + requires (!std::is_void_v) + [[nodiscard]] constexpr const E& value() const noexcept { + return value_.get(); + } + + // Comparison operators - compare referenced values, not references themselves + constexpr bool operator==(const edge_descriptor& other) const noexcept { + if constexpr (std::is_void_v) { + return source_id_ == other.source_id_ && target_id_ == other.target_id_; + } else { + return source_id_ == other.source_id_ && + target_id_ == other.target_id_ && + value_.get() == other.value_.get(); + } + } + + constexpr auto operator<=>(const edge_descriptor& other) const noexcept { + if (auto cmp = source_id_ <=> other.source_id_; cmp != 0) return cmp; + if (auto cmp = target_id_ <=> other.target_id_; cmp != 0) return cmp; + if constexpr (!std::is_void_v) { + return value_.get() <=> other.value_.get(); + } else { + return std::strong_ordering::equal; + } + } + +private: + const VId& source_id_; + const VId& target_id_; + [[no_unique_address]] std::conditional_t, + detail::empty_value, std::reference_wrapper> value_; +}; + +// Deduction guides +template +edge_descriptor(VId, VId) -> edge_descriptor; + +template +edge_descriptor(VId, VId, EV) -> edge_descriptor; + +} // namespace graph::edge_list diff --git a/include/graph/edge_list/edge_list_traits.hpp b/include/graph/edge_list/edge_list_traits.hpp new file mode 100644 index 0000000..81d2331 --- /dev/null +++ b/include/graph/edge_list/edge_list_traits.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace graph::edge_list { + +// Forward declaration - actual type defined in edge_list_descriptor.hpp +template +struct edge_descriptor; + +// Type trait to identify edge_list descriptors +template +struct is_edge_list_descriptor : std::false_type {}; + +template +struct is_edge_list_descriptor> : std::true_type {}; + +template +inline constexpr bool is_edge_list_descriptor_v = is_edge_list_descriptor::value; + +} // namespace graph::edge_list diff --git a/include/graph/edgelist.hpp b/include/graph/edgelist.hpp deleted file mode 100644 index 4cec892..0000000 --- a/include/graph/edgelist.hpp +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @file edgelist.hpp - * @brief Edge list container and utilities - * - * This file contains the edge list container - a simple, flexible - * representation of a graph as a list of edges. - */ - -#pragma once - -#include "adj_list/descriptor.hpp" -#include "detail/graph_using.hpp" -#include -#include -#include - -namespace graph { - -/** - * @brief Simple edge representation with source and target vertices - * - * @tparam VId Vertex identifier type - * @tparam EV Edge value type (default: empty) - */ -template -struct edge { - VId source; - VId target; - - // Edge value storage (when EV is not void) - [[no_unique_address]] - std::conditional_t, std::monostate, EV> value; - - constexpr edge() = default; - - // Constructor for edges without values - constexpr edge(VId src, VId tgt) - requires std::is_void_v - : source(src), target(tgt) {} - - // Constructor for edges with values - constexpr edge(VId src, VId tgt, const EV& val) - requires (!std::is_void_v) - : source(src), target(tgt), value(val) {} - - constexpr edge(VId src, VId tgt, EV&& val) - requires (!std::is_void_v) - : source(src), target(tgt), value(std::move(val)) {} -}; - -// Deduction guides -template -edge(VId, VId) -> edge; - -template -edge(VId, VId, EV) -> edge; - -/** - * @brief Edge list graph container - * - * A simple graph representation as a vector of edges. - * Suitable for algorithms that need to iterate over all edges. - * - * @tparam VId Vertex identifier type - * @tparam EV Edge value type - * @tparam Alloc Allocator type - */ -template>> -class edgelist { -public: - using vertex_id_type = VId; - using edge_value_type = EV; - using edge_type = edge; - using container_type = std::vector; - using size_type = typename container_type::size_type; - using iterator = typename container_type::iterator; - using const_iterator = typename container_type::const_iterator; - -private: - container_type edges_; - size_type num_vertices_ = 0; - -public: - // Constructors - edgelist() = default; - explicit edgelist(size_type num_verts) : num_vertices_(num_verts) {} - - // Edge access - auto begin() { return edges_.begin(); } - auto end() { return edges_.end(); } - auto begin() const { return edges_.begin(); } - auto end() const { return edges_.end(); } - auto cbegin() const { return edges_.cbegin(); } - auto cend() const { return edges_.cend(); } - - // Size queries - size_type num_edges() const { return edges_.size(); } - size_type num_vertices() const { return num_vertices_; } - bool empty() const { return edges_.empty(); } - - // Modifiers - void add_edge(VId src, VId tgt) requires std::is_void_v { - edges_.emplace_back(src, tgt); - num_vertices_ = std::max(num_vertices_, - static_cast(std::max(src, tgt) + 1)); - } - - void add_edge(VId src, VId tgt, const EV& val) requires (!std::is_void_v) { - edges_.emplace_back(src, tgt, val); - num_vertices_ = std::max(num_vertices_, - static_cast(std::max(src, tgt) + 1)); - } - - void add_edge(VId src, VId tgt, EV&& val) requires (!std::is_void_v) { - edges_.emplace_back(src, tgt, std::move(val)); - num_vertices_ = std::max(num_vertices_, - static_cast(std::max(src, tgt) + 1)); - } - - void reserve(size_type n) { edges_.reserve(n); } - void clear() { edges_.clear(); } - - void set_num_vertices(size_type n) { num_vertices_ = n; } -}; - -} // namespace graph diff --git a/include/graph/graph.hpp b/include/graph/graph.hpp index eaa0853..e7d4cdc 100644 --- a/include/graph/graph.hpp +++ b/include/graph/graph.hpp @@ -30,7 +30,11 @@ // Graph information and utilities (shared between adj_list and edge_list) #include -#include + +// Edge list interface +#include +#include +#include // Adjacency list interface #include @@ -50,10 +54,6 @@ // #include // #include -// Future: View implementations will be included here -// #include -// #include - /** * @namespace graph * @brief Root namespace for the graph library diff --git a/include/graph/graph_info.hpp b/include/graph/graph_info.hpp index 5f7ff71..d8993f6 100644 --- a/include/graph/graph_info.hpp +++ b/include/graph/graph_info.hpp @@ -36,6 +36,8 @@ struct vertex_info { vertex_type vertex; value_type value; }; + +// Specializations with VId present template struct vertex_info { using id_type = VId; @@ -63,6 +65,39 @@ struct vertex_info { id_type id; }; +// Specializations with VId=void (descriptor-based pattern) +template +struct vertex_info { + using id_type = void; + using vertex_type = V; + using value_type = VV; + + vertex_type vertex; + value_type value; +}; +template +struct vertex_info { + using id_type = void; + using vertex_type = V; + using value_type = void; + + vertex_type vertex; +}; +template +struct vertex_info { + using id_type = void; + using vertex_type = void; + using value_type = VV; + + value_type value; +}; +template <> +struct vertex_info { + using id_type = void; + using vertex_type = void; + using value_type = void; +}; + template using copyable_vertex_t = vertex_info; // {id, value} @@ -88,6 +123,7 @@ struct edge_info { value_type value; }; +// Sourced=true specializations with VId present template struct edge_info { using source_id_type = VId; @@ -121,6 +157,7 @@ struct edge_info { value_type value; }; +// Sourced=false specializations with VId present template struct edge_info { using source_id_type = void; @@ -163,6 +200,80 @@ struct edge_info { target_id_type target_id; }; +// Sourced=true specializations with VId=void (descriptor-based pattern) +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = E; + using value_type = EV; + + edge_type edge; + value_type value; +}; +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = E; + using value_type = void; + + edge_type edge; +}; +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = void; + using value_type = EV; + + value_type value; +}; +template <> +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = void; + using value_type = void; +}; + +// Sourced=false specializations with VId=void (descriptor-based pattern) +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = E; + using value_type = EV; + + edge_type edge; + value_type value; +}; +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = E; + using value_type = void; + + edge_type edge; +}; +template +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = void; + using value_type = EV; + + value_type value; +}; +template <> +struct edge_info { + using source_id_type = void; + using target_id_type = void; + using edge_type = void; + using value_type = void; +}; + // // targeted_edge // for(auto&& [vid,uv,value] : edges_view(g, u, [](edge_t uv) { return ...; } ) @@ -209,6 +320,7 @@ struct neighbor_info { value_type value; }; +// Sourced=false specializations with VId present template struct neighbor_info { using source_id_type = void; @@ -253,6 +365,7 @@ struct neighbor_info { target_id_type target_id; }; +// Sourced=true specializations with VId present template struct neighbor_info { using source_id_type = VId; @@ -288,6 +401,86 @@ struct neighbor_info { target_id_type target_id; }; +// Sourced=false specializations with VId=void (descriptor-based pattern) +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = V; + using value_type = VV; + + vertex_type vertex; + value_type value; +}; + +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = V; + using value_type = void; + + vertex_type vertex; +}; + +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = void; + using value_type = VV; + + value_type value; +}; + +template <> +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = void; + using value_type = void; +}; + +// Sourced=true specializations with VId=void (descriptor-based pattern) +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = V; + using value_type = VV; + + vertex_type vertex; + value_type value; +}; + +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = V; + using value_type = void; + + vertex_type vertex; +}; + +template +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = void; + using value_type = VV; + + value_type value; +}; + +template <> +struct neighbor_info { + using source_id_type = void; + using target_id_type = void; + using vertex_type = void; + using value_type = void; +}; + // // copyable_edge_t // diff --git a/include/graph/views.hpp b/include/graph/views.hpp new file mode 100644 index 0000000..d02b387 --- /dev/null +++ b/include/graph/views.hpp @@ -0,0 +1,72 @@ +/** + * @file views.hpp + * @brief Unified header for all graph views + * + * This master header includes all graph view implementations, providing + * lazy, range-based access to graph elements using C++20 ranges and + * structured bindings. + * + * @par View Categories + * + * **Basic Views:** + * - vertexlist: View of all vertices + * - incidence: View of edges from a vertex + * - neighbors: View of neighbor vertices + * - edgelist: View of all edges + * + * **Search Views:** + * - vertices_dfs: Depth-first vertex traversal + * - edges_dfs: Depth-first edge traversal + * - vertices_bfs: Breadth-first vertex traversal + * - edges_bfs: Breadth-first edge traversal + * - vertices_topological_sort: Topological order vertex traversal + * - edges_topological_sort: Topological order edge traversal + * + * **Range Adaptors:** + * All views support C++20 pipe syntax via adaptors: + * @code + * auto g = make_graph(); + * + * // Basic views with pipe syntax + * for (auto [v] : g | vertexlist()) { ... } + * for (auto [e] : g | incidence(uid)) { ... } + * for (auto [v] : g | neighbors(uid)) { ... } + * for (auto [e] : g | edgelist()) { ... } + * + * // Search views with pipe syntax + * for (auto [v] : g | vertices_dfs(seed)) { ... } + * for (auto [e] : g | edges_bfs(seed)) { ... } + * for (auto [v] : g | vertices_topological_sort()) { ... } + * + * // Chaining with standard range adaptors + * for (auto id : g | vertexlist() + * | std::views::transform(...) + * | std::views::filter(...)) { ... } + * @endcode + * + * @par Value Functions + * All views support optional value functions that transform the output: + * @code + * auto vvf = [](auto v) { return some_value; }; + * for (auto [v, val] : g | vertexlist(vvf)) { ... } + * @endcode + * + * @see graph::views::adaptors Namespace containing all adaptor objects + */ + +#pragma once + +// Basic views (vertexlist, incidence, neighbors, edgelist) +#include + +// Depth-first search views +#include + +// Breadth-first search views +#include + +// Topological sort views +#include + +// Range adaptor closures for pipe syntax +#include diff --git a/include/graph/views/adaptors.hpp b/include/graph/views/adaptors.hpp new file mode 100644 index 0000000..21e7e48 --- /dev/null +++ b/include/graph/views/adaptors.hpp @@ -0,0 +1,611 @@ +/** + * @file adaptors.hpp + * @brief Range adaptor closures for graph views enabling pipe syntax + * + * Implements C++20 range adaptor pattern for graph views: + * + * Basic views: + * - g | vertexlist() - view of all vertices + * - g | incidence(uid) - view of edges from a vertex + * - g | neighbors(uid) - view of neighbor vertices + * - g | edgelist() - view of all edges + * + * Search views: + * - g | vertices_dfs(seed) - depth-first vertex traversal + * - g | edges_dfs(seed) - depth-first edge traversal + * - g | vertices_bfs(seed) - breadth-first vertex traversal + * - g | edges_bfs(seed) - breadth-first edge traversal + * - g | vertices_topological_sort() - topological order vertex traversal + * - g | edges_topological_sort() - topological order edge traversal + * + * Supports chaining with standard range adaptors: + * - g | vertexlist() | std::views::take(5) + * - g | incidence(0) | std::views::filter(predicate) + * - g | vertices_dfs(0) | std::views::transform(fn) + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Empty placeholder for void template parameters +struct monostate {}; + +//============================================================================= +// vertexlist adaptor +//============================================================================= + +template +struct vertexlist_adaptor_closure { + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + + template + friend auto operator|(G&& g, vertexlist_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return vertexlist(std::forward(g)); + } else { + return vertexlist(std::forward(g), std::move(adaptor.vvf)); + } + } +}; + +// Factory function object for creating vertexlist adaptors +struct vertexlist_adaptor_fn { + // No arguments: g | vertexlist() + auto operator()() const { + return vertexlist_adaptor_closure{monostate{}}; + } + + // With value function: g | vertexlist(vvf) + template + auto operator()(VVF&& vvf) const { + return vertexlist_adaptor_closure>{std::forward(vvf)}; + } + + // Direct call: vertexlist(g) + template + auto operator()(G&& g) const { + return graph::views::vertexlist(std::forward(g)); + } + + // Direct call with value function: vertexlist(g, vvf) + template + auto operator()(G&& g, VVF&& vvf) const { + return graph::views::vertexlist(std::forward(g), std::forward(vvf)); + } +}; + +//============================================================================= +// incidence adaptor +//============================================================================= + +template +struct incidence_adaptor_closure { + UID uid; + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + + template + friend auto operator|(G&& g, incidence_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return incidence(std::forward(g), std::move(adaptor.uid)); + } else { + return incidence(std::forward(g), std::move(adaptor.uid), std::move(adaptor.evf)); + } + } +}; + +// Factory function object for creating incidence adaptors +struct incidence_adaptor_fn { + // With vertex id: g | incidence(uid) + template + auto operator()(UID&& uid) const { + return incidence_adaptor_closure, void>{std::forward(uid), monostate{}}; + } + + // With vertex id and value function: g | incidence(uid, evf) + template + auto operator()(UID&& uid, EVF&& evf) const { + return incidence_adaptor_closure, std::decay_t>{ + std::forward(uid), std::forward(evf)}; + } + + // Direct call: incidence(g, uid) + template + auto operator()(G&& g, UID&& uid) const { + return graph::views::incidence(std::forward(g), std::forward(uid)); + } + + // Direct call with value function: incidence(g, uid, evf) + template + auto operator()(G&& g, UID&& uid, EVF&& evf) const { + return graph::views::incidence(std::forward(g), std::forward(uid), std::forward(evf)); + } +}; + +//============================================================================= +// neighbors adaptor +//============================================================================= + +template +struct neighbors_adaptor_closure { + UID uid; + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + + template + friend auto operator|(G&& g, neighbors_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return neighbors(std::forward(g), std::move(adaptor.uid)); + } else { + return neighbors(std::forward(g), std::move(adaptor.uid), std::move(adaptor.vvf)); + } + } +}; + +// Factory function object for creating neighbors adaptors +struct neighbors_adaptor_fn { + // With vertex id: g | neighbors(uid) + template + auto operator()(UID&& uid) const { + return neighbors_adaptor_closure, void>{std::forward(uid), monostate{}}; + } + + // With vertex id and value function: g | neighbors(uid, vvf) + template + auto operator()(UID&& uid, VVF&& vvf) const { + return neighbors_adaptor_closure, std::decay_t>{ + std::forward(uid), std::forward(vvf)}; + } + + // Direct call: neighbors(g, uid) + template + auto operator()(G&& g, UID&& uid) const { + return graph::views::neighbors(std::forward(g), std::forward(uid)); + } + + // Direct call with value function: neighbors(g, uid, vvf) + template + auto operator()(G&& g, UID&& uid, VVF&& vvf) const { + return graph::views::neighbors(std::forward(g), std::forward(uid), std::forward(vvf)); + } +}; + +//============================================================================= +// edgelist adaptor +//============================================================================= + +template +struct edgelist_adaptor_closure { + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + + template + friend auto operator|(G&& g, edgelist_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return edgelist(std::forward(g)); + } else { + return edgelist(std::forward(g), std::move(adaptor.evf)); + } + } +}; + +// Factory function object for creating edgelist adaptors +struct edgelist_adaptor_fn { + // No arguments: g | edgelist() + auto operator()() const { + return edgelist_adaptor_closure{monostate{}}; + } + + // With value function: g | edgelist(evf) + template + auto operator()(EVF&& evf) const { + return edgelist_adaptor_closure>{std::forward(evf)}; + } + + // Direct call: edgelist(g) + template + auto operator()(G&& g) const { + return graph::views::edgelist(std::forward(g)); + } + + // Direct call with value function: edgelist(g, evf) + template + auto operator()(G&& g, EVF&& evf) const { + return graph::views::edgelist(std::forward(g), std::forward(evf)); + } +}; + +//============================================================================= +// vertices_dfs adaptor +//============================================================================= + +template> +struct vertices_dfs_adaptor_closure { + Seed seed; + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + [[no_unique_address]] Alloc alloc; + + template + friend auto operator|(G&& g, vertices_dfs_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return vertices_dfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.alloc)); + } else { + return vertices_dfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.vvf), std::move(adaptor.alloc)); + } + } +}; + +// Factory function object for creating vertices_dfs adaptors +struct vertices_dfs_adaptor_fn { + // With seed: g | vertices_dfs(seed) + template + auto operator()(Seed&& seed) const { + return vertices_dfs_adaptor_closure, void, std::allocator>{ + std::forward(seed), monostate{}, std::allocator{}}; + } + + // With seed and value function: g | vertices_dfs(seed, vvf) + template + auto operator()(Seed&& seed, VVF&& vvf) const { + return vertices_dfs_adaptor_closure, std::decay_t, std::allocator>{ + std::forward(seed), std::forward(vvf), std::allocator{}}; + } + + // With seed, value function, and allocator: g | vertices_dfs(seed, vvf, alloc) + template + auto operator()(Seed&& seed, VVF&& vvf, Alloc&& alloc) const { + return vertices_dfs_adaptor_closure, std::decay_t, std::decay_t>{ + std::forward(seed), std::forward(vvf), std::forward(alloc)}; + } + + // Direct call: vertices_dfs(g, seed) + template + auto operator()(G&& g, Seed&& seed) const { + return graph::views::vertices_dfs(std::forward(g), std::forward(seed)); + } + + // Direct call with value function: vertices_dfs(g, seed, vvf) + template + auto operator()(G&& g, Seed&& seed, VVF&& vvf) const { + return graph::views::vertices_dfs(std::forward(g), std::forward(seed), std::forward(vvf)); + } + + // Direct call with value function and allocator: vertices_dfs(g, seed, vvf, alloc) + template + auto operator()(G&& g, Seed&& seed, VVF&& vvf, Alloc&& alloc) const { + return graph::views::vertices_dfs(std::forward(g), std::forward(seed), std::forward(vvf), std::forward(alloc)); + } +}; + +//============================================================================= +// edges_dfs adaptor +//============================================================================= + +template> +struct edges_dfs_adaptor_closure { + Seed seed; + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + [[no_unique_address]] Alloc alloc; + + template + friend auto operator|(G&& g, edges_dfs_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return edges_dfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.alloc)); + } else { + return edges_dfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.evf), std::move(adaptor.alloc)); + } + } +}; + +// Factory function object for creating edges_dfs adaptors +struct edges_dfs_adaptor_fn { + // With seed: g | edges_dfs(seed) + template + auto operator()(Seed&& seed) const { + return edges_dfs_adaptor_closure, void, std::allocator>{ + std::forward(seed), monostate{}, std::allocator{}}; + } + + // With seed and value function: g | edges_dfs(seed, evf) + template + auto operator()(Seed&& seed, EVF&& evf) const { + return edges_dfs_adaptor_closure, std::decay_t, std::allocator>{ + std::forward(seed), std::forward(evf), std::allocator{}}; + } + + // With seed, value function, and allocator: g | edges_dfs(seed, evf, alloc) + template + auto operator()(Seed&& seed, EVF&& evf, Alloc&& alloc) const { + return edges_dfs_adaptor_closure, std::decay_t, std::decay_t>{ + std::forward(seed), std::forward(evf), std::forward(alloc)}; + } + + // Direct call: edges_dfs(g, seed) + template + auto operator()(G&& g, Seed&& seed) const { + return graph::views::edges_dfs(std::forward(g), std::forward(seed)); + } + + // Direct call with value function: edges_dfs(g, seed, evf) + template + auto operator()(G&& g, Seed&& seed, EVF&& evf) const { + return graph::views::edges_dfs(std::forward(g), std::forward(seed), std::forward(evf)); + } + + // Direct call with value function and allocator: edges_dfs(g, seed, evf, alloc) + template + auto operator()(G&& g, Seed&& seed, EVF&& evf, Alloc&& alloc) const { + return graph::views::edges_dfs(std::forward(g), std::forward(seed), std::forward(evf), std::forward(alloc)); + } +}; + +//============================================================================= +// vertices_bfs adaptor +//============================================================================= + +template> +struct vertices_bfs_adaptor_closure { + Seed seed; + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + [[no_unique_address]] Alloc alloc; + + template + friend auto operator|(G&& g, vertices_bfs_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return vertices_bfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.alloc)); + } else { + return vertices_bfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.vvf), std::move(adaptor.alloc)); + } + } +}; + +// Factory function object for creating vertices_bfs adaptors +struct vertices_bfs_adaptor_fn { + // With seed: g | vertices_bfs(seed) + template + auto operator()(Seed&& seed) const { + return vertices_bfs_adaptor_closure, void, std::allocator>{ + std::forward(seed), monostate{}, std::allocator{}}; + } + + // With seed and value function: g | vertices_bfs(seed, vvf) + template + auto operator()(Seed&& seed, VVF&& vvf) const { + return vertices_bfs_adaptor_closure, std::decay_t, std::allocator>{ + std::forward(seed), std::forward(vvf), std::allocator{}}; + } + + // With seed, value function, and allocator: g | vertices_bfs(seed, vvf, alloc) + template + auto operator()(Seed&& seed, VVF&& vvf, Alloc&& alloc) const { + return vertices_bfs_adaptor_closure, std::decay_t, std::decay_t>{ + std::forward(seed), std::forward(vvf), std::forward(alloc)}; + } + + // Direct call: vertices_bfs(g, seed) + template + auto operator()(G&& g, Seed&& seed) const { + return graph::views::vertices_bfs(std::forward(g), std::forward(seed)); + } + + // Direct call with value function: vertices_bfs(g, seed, vvf) + template + auto operator()(G&& g, Seed&& seed, VVF&& vvf) const { + return graph::views::vertices_bfs(std::forward(g), std::forward(seed), std::forward(vvf)); + } + + // Direct call with value function and allocator: vertices_bfs(g, seed, vvf, alloc) + template + auto operator()(G&& g, Seed&& seed, VVF&& vvf, Alloc&& alloc) const { + return graph::views::vertices_bfs(std::forward(g), std::forward(seed), std::forward(vvf), std::forward(alloc)); + } +}; + +//============================================================================= +// edges_bfs adaptor +//============================================================================= + +template> +struct edges_bfs_adaptor_closure { + Seed seed; + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + [[no_unique_address]] Alloc alloc; + + template + friend auto operator|(G&& g, edges_bfs_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return edges_bfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.alloc)); + } else { + return edges_bfs(std::forward(g), std::move(adaptor.seed), std::move(adaptor.evf), std::move(adaptor.alloc)); + } + } +}; + +// Factory function object for creating edges_bfs adaptors +struct edges_bfs_adaptor_fn { + // With seed: g | edges_bfs(seed) + template + auto operator()(Seed&& seed) const { + return edges_bfs_adaptor_closure, void, std::allocator>{ + std::forward(seed), monostate{}, std::allocator{}}; + } + + // With seed and value function: g | edges_bfs(seed, evf) + template + auto operator()(Seed&& seed, EVF&& evf) const { + return edges_bfs_adaptor_closure, std::decay_t, std::allocator>{ + std::forward(seed), std::forward(evf), std::allocator{}}; + } + + // With seed, value function, and allocator: g | edges_bfs(seed, evf, alloc) + template + auto operator()(Seed&& seed, EVF&& evf, Alloc&& alloc) const { + return edges_bfs_adaptor_closure, std::decay_t, std::decay_t>{ + std::forward(seed), std::forward(evf), std::forward(alloc)}; + } + + // Direct call: edges_bfs(g, seed) + template + auto operator()(G&& g, Seed&& seed) const { + return graph::views::edges_bfs(std::forward(g), std::forward(seed)); + } + + // Direct call with value function: edges_bfs(g, seed, evf) + template + auto operator()(G&& g, Seed&& seed, EVF&& evf) const { + return graph::views::edges_bfs(std::forward(g), std::forward(seed), std::forward(evf)); + } + + // Direct call with value function and allocator: edges_bfs(g, seed, evf, alloc) + template + auto operator()(G&& g, Seed&& seed, EVF&& evf, Alloc&& alloc) const { + return graph::views::edges_bfs(std::forward(g), std::forward(seed), std::forward(evf), std::forward(alloc)); + } +}; + +//============================================================================= +// vertices_topological_sort adaptor +//============================================================================= + +template> +struct vertices_topological_sort_adaptor_closure { + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + [[no_unique_address]] Alloc alloc; + + template + friend auto operator|(G&& g, vertices_topological_sort_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return graph::views::vertices_topological_sort(std::forward(g), adaptor.alloc); + } else { + return graph::views::vertices_topological_sort(std::forward(g), adaptor.vvf, adaptor.alloc); + } + } +}; + +struct vertices_topological_sort_adaptor_fn { + // Basic: g | vertices_topological_sort() + auto operator()() const { + return vertices_topological_sort_adaptor_closure>{}; + } + + // With value function: g | vertices_topological_sort(vvf) + template + auto operator()(VVF&& vvf) const { + return vertices_topological_sort_adaptor_closure, std::allocator>{ + std::forward(vvf), std::allocator{}}; + } + + // With value function and allocator: g | vertices_topological_sort(vvf, alloc) + template + auto operator()(VVF&& vvf, Alloc&& alloc) const { + return vertices_topological_sort_adaptor_closure, std::decay_t>{ + std::forward(vvf), std::forward(alloc)}; + } + + // Direct call: vertices_topological_sort(g) + template + auto operator()(G&& g) const { + return graph::views::vertices_topological_sort(std::forward(g)); + } + + // Direct call with value function: vertices_topological_sort(g, vvf) + template + auto operator()(G&& g, VVF&& vvf) const { + return graph::views::vertices_topological_sort(std::forward(g), std::forward(vvf)); + } + + // Direct call with value function and allocator: vertices_topological_sort(g, vvf, alloc) + template + auto operator()(G&& g, VVF&& vvf, Alloc&& alloc) const { + return graph::views::vertices_topological_sort(std::forward(g), std::forward(vvf), std::forward(alloc)); + } +}; + +//============================================================================= +// edges_topological_sort adaptor +//============================================================================= + +template> +struct edges_topological_sort_adaptor_closure { + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + [[no_unique_address]] Alloc alloc; + + template + friend auto operator|(G&& g, edges_topological_sort_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return graph::views::edges_topological_sort(std::forward(g), adaptor.alloc); + } else { + return graph::views::edges_topological_sort(std::forward(g), adaptor.evf, adaptor.alloc); + } + } +}; + +struct edges_topological_sort_adaptor_fn { + // Basic: g | edges_topological_sort() + auto operator()() const { + return edges_topological_sort_adaptor_closure>{}; + } + + // With value function: g | edges_topological_sort(evf) + template + auto operator()(EVF&& evf) const { + return edges_topological_sort_adaptor_closure, std::allocator>{ + std::forward(evf), std::allocator{}}; + } + + // With value function and allocator: g | edges_topological_sort(evf, alloc) + template + auto operator()(EVF&& evf, Alloc&& alloc) const { + return edges_topological_sort_adaptor_closure, std::decay_t>{ + std::forward(evf), std::forward(alloc)}; + } + + // Direct call: edges_topological_sort(g) + template + auto operator()(G&& g) const { + return graph::views::edges_topological_sort(std::forward(g)); + } + + // Direct call with value function: edges_topological_sort(g, evf) + template + auto operator()(G&& g, EVF&& evf) const { + return graph::views::edges_topological_sort(std::forward(g), std::forward(evf)); + } + + // Direct call with value function and allocator: edges_topological_sort(g, evf, alloc) + template + auto operator()(G&& g, EVF&& evf, Alloc&& alloc) const { + return graph::views::edges_topological_sort(std::forward(g), std::forward(evf), std::forward(alloc)); + } +}; + +} // namespace graph::views + +//============================================================================= +// Adaptor objects for pipe syntax +//============================================================================= + +namespace graph::views::adaptors { + // Basic views + inline constexpr vertexlist_adaptor_fn vertexlist{}; + inline constexpr incidence_adaptor_fn incidence{}; + inline constexpr neighbors_adaptor_fn neighbors{}; + inline constexpr edgelist_adaptor_fn edgelist{}; + + // Search views + inline constexpr vertices_dfs_adaptor_fn vertices_dfs{}; + inline constexpr edges_dfs_adaptor_fn edges_dfs{}; + inline constexpr vertices_bfs_adaptor_fn vertices_bfs{}; + inline constexpr edges_bfs_adaptor_fn edges_bfs{}; + + // Topological sort views + inline constexpr vertices_topological_sort_adaptor_fn vertices_topological_sort{}; + inline constexpr edges_topological_sort_adaptor_fn edges_topological_sort{}; +} // namespace graph::views::adaptors diff --git a/include/graph/views/basic_views.hpp b/include/graph/views/basic_views.hpp new file mode 100644 index 0000000..9258c9d --- /dev/null +++ b/include/graph/views/basic_views.hpp @@ -0,0 +1,17 @@ +#pragma once + +/** + * @file basic_views.hpp + * @brief Convenience header including all basic graph views. + * + * This header provides a single include for all basic (non-search) graph views: + * - vertexlist: Iterate over vertices with optional value function + * - incidence: Iterate over edges incident to vertices + * - neighbors: Iterate over neighboring vertices + * - edgelist: Iterate over all edges (adjacency_list or edge_list) + */ + +#include +#include +#include +#include diff --git a/include/graph/views/bfs.hpp b/include/graph/views/bfs.hpp new file mode 100644 index 0000000..8dd31f0 --- /dev/null +++ b/include/graph/views/bfs.hpp @@ -0,0 +1,1081 @@ +/** + * @file bfs.hpp + * @brief Breadth-first search views for vertices and edges + * + * Provides views that traverse a graph in breadth-first order from a seed vertex, + * yielding vertex_info or edge_info for each visited element. + * + * @complexity Time: O(V + E) where V is reachable vertices and E is reachable edges + * BFS visits each reachable vertex once and traverses each reachable edge once + * @complexity Space: O(V) for the queue and visited tracker + * + * @par Examples: + * @code + * // Vertex traversal + * for (auto [v] : vertices_bfs(g, seed)) + * process_vertex(v); + * + * // Vertex traversal with value function + * for (auto [v, val] : vertices_bfs(g, seed, value_fn)) + * process_vertex_with_value(v, val); + * + * // Access depth during traversal + * auto bfs = vertices_bfs(g, seed); + * for (auto [v] : bfs) + * std::cout << "Vertex " << vertex_id(g, v) << " at depth " << bfs.depth() << "\n"; + * + * // Cancel search + * auto bfs = vertices_bfs(g, seed); + * for (auto [v] : bfs) { + * if (should_stop(v)) + * bfs.cancel(cancel_search::cancel_all); + * } + * @endcode + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declarations +template > +class vertices_bfs_view; + +template > +class edges_bfs_view; + +namespace bfs_detail { + +/// Queue entry for BFS traversal: stores vertex descriptor and its depth +template +struct queue_entry { + Vertex vertex; + std::size_t depth; +}; + +/// Queue entry for BFS edge traversal: stores vertex, depth, and edge iteration state +template +struct edge_queue_entry { + Vertex vertex; + std::size_t depth; + EdgeIter edge_end; + EdgeIter edge_current; +}; + +/** + * @brief Shared BFS state for vertex traversal + * + * @complexity Time: O(V + E) - visits each reachable vertex once, traverses each reachable edge once + * @complexity Space: O(V) - queue stores up to V vertices, visited tracker uses O(V) space + */ +template +struct bfs_state { + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using allocator_type = Alloc; + using entry_type = queue_entry; + using queue_alloc = typename std::allocator_traits::template rebind_alloc; + + std::queue> queue_; + visited_tracker visited_; + cancel_search cancel_ = cancel_search::continue_search; + std::size_t max_depth_ = 0; + std::size_t count_ = 0; + + bfs_state(G& g, vertex_type seed_vertex, std::size_t num_vertices, Alloc alloc = {}) + : queue_(std::deque(alloc)) + , visited_(num_vertices, alloc) + { + queue_.push({seed_vertex, 0}); + visited_.mark_visited(adj_list::vertex_id(g, seed_vertex)); + } +}; + +/** + * @brief Shared BFS state for edge traversal + * + * @complexity Time: O(V + E) - visits each reachable vertex once, traverses each reachable edge once + * @complexity Space: O(V) - queue and visited tracker use O(V) space + */ +template +struct bfs_edge_state { + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_iter_type = adj_list::vertex_edge_iterator_t; + using allocator_type = Alloc; + using entry_type = edge_queue_entry; + using queue_alloc = typename std::allocator_traits::template rebind_alloc; + + std::queue> queue_; + visited_tracker visited_; + cancel_search cancel_ = cancel_search::continue_search; + std::size_t max_depth_ = 0; + std::size_t count_ = 0; + std::optional skip_vertex_id_; // Vertex to skip when processing (for cancel_branch) + + bfs_edge_state(G& g, vertex_type seed_vertex, std::size_t num_vertices, Alloc alloc = {}) + : queue_(std::deque(alloc)) + , visited_(num_vertices, alloc) + { + auto edge_range = adj_list::edges(g, seed_vertex); + auto edge_begin = std::ranges::begin(edge_range); + auto edge_end = std::ranges::end(edge_range); + queue_.push({seed_vertex, 0, edge_end, edge_begin}); + visited_.mark_visited(adj_list::vertex_id(g, seed_vertex)); + } +}; + +} // namespace bfs_detail + +/** + * @brief BFS vertex view without value function + * + * Iterates over vertices in breadth-first order yielding + * vertex_info, void> + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam Alloc Allocator type for internal containers + */ +template +class vertices_bfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using info_type = vertex_info; + +private: + using state_type = bfs_detail::bfs_state; + +public: + /** + * @brief Input iterator for BFS vertex traversal + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state) + : g_(g), state_(std::move(state)) {} + + [[nodiscard]] value_type operator*() const { + return value_type{state_->queue_.front().vertex}; + } + + iterator& operator++() { + advance(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || state_->queue_.empty(); + bool other_at_end = !other.state_ || other.state_->queue_.empty(); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || state_->queue_.empty(); + } + + private: + void advance() { + if (!state_ || state_->queue_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->queue_.empty()) state_->queue_.pop(); + return; + } + + // Get current vertex and its depth + auto current = state_->queue_.front(); + state_->queue_.pop(); + + // Handle cancel_branch: skip exploring children of current vertex + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->cancel_ = cancel_search::continue_search; + return; // Don't enqueue children, just continue to next in queue + } + + // Explore neighbors (enqueue unvisited ones) + auto edge_range = adj_list::edges(*g_, current.vertex); + for (auto edge : edge_range) { + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + state_->queue_.push({target_v, current.depth + 1}); + if (current.depth + 1 > state_->max_depth_) { + state_->max_depth_ = current.depth + 1; + } + ++state_->count_; + } + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + }; + + /// Sentinel for end of BFS traversal + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr vertices_bfs_view() noexcept = default; + + /// Construct from vertex descriptor + vertices_bfs_view(G& g, vertex_type seed_vertex, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + vertices_bfs_view(G& g, vertex_id_type seed, Alloc alloc = {}) + : vertices_bfs_view(g, *adj_list::find_vertex(g, seed), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + /// Get current cancel state + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + /// Set cancel state to stop traversal + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + /// Get maximum depth reached so far + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->max_depth_ : 0; + } + + /// Get count of vertices visited so far + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + std::shared_ptr state_; +}; + +/** + * @brief BFS vertex view with value function + * + * Iterates over vertices in breadth-first order yielding + * vertex_info, VV> where VV is the invoke result of VVF. + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam VVF Vertex value function type + * @tparam Alloc Allocator type for internal containers + */ +template +class vertices_bfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using value_result_type = std::invoke_result_t; + using info_type = vertex_info; + +private: + using state_type = bfs_detail::bfs_state; + +public: + /** + * @brief Input iterator for BFS vertex traversal with value function + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state, VVF* vvf) + : g_(g), state_(std::move(state)), vvf_(vvf) {} + + [[nodiscard]] value_type operator*() const { + auto v = state_->queue_.front().vertex; + return value_type{v, std::invoke(*vvf_, v)}; + } + + iterator& operator++() { + advance(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || state_->queue_.empty(); + bool other_at_end = !other.state_ || other.state_->queue_.empty(); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || state_->queue_.empty(); + } + + private: + void advance() { + if (!state_ || state_->queue_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->queue_.empty()) state_->queue_.pop(); + return; + } + + auto current = state_->queue_.front(); + state_->queue_.pop(); + + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->cancel_ = cancel_search::continue_search; + return; + } + + auto edge_range = adj_list::edges(*g_, current.vertex); + for (auto edge : edge_range) { + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + state_->queue_.push({target_v, current.depth + 1}); + if (current.depth + 1 > state_->max_depth_) { + state_->max_depth_ = current.depth + 1; + } + ++state_->count_; + } + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + VVF* vvf_ = nullptr; + }; + + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr vertices_bfs_view() noexcept = default; + + /// Construct from vertex descriptor + vertices_bfs_view(G& g, vertex_type seed_vertex, VVF vvf, Alloc alloc = {}) + : g_(&g) + , vvf_(std::move(vvf)) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + vertices_bfs_view(G& g, vertex_id_type seed, VVF vvf, Alloc alloc = {}) + : vertices_bfs_view(g, *adj_list::find_vertex(g, seed), std::move(vvf), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_, &vvf_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->max_depth_ : 0; + } + + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + [[no_unique_address]] VVF vvf_{}; + std::shared_ptr state_; +}; + +// Deduction guides for vertex_id +template > +vertices_bfs_view(G&, adj_list::vertex_id_t, Alloc) -> vertices_bfs_view; + +template +vertices_bfs_view(G&, adj_list::vertex_id_t) -> vertices_bfs_view>; + +template > +vertices_bfs_view(G&, adj_list::vertex_id_t, VVF, Alloc) -> vertices_bfs_view; + +// Deduction guides for vertex descriptor +template > +vertices_bfs_view(G&, adj_list::vertex_t, Alloc) -> vertices_bfs_view; + +template +vertices_bfs_view(G&, adj_list::vertex_t) -> vertices_bfs_view>; + +template > +vertices_bfs_view(G&, adj_list::vertex_t, VVF, Alloc) -> vertices_bfs_view; + +/** + * @brief Create a BFS vertex view without value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @return vertices_bfs_view yielding vertex_info + */ +template +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_id_t seed) { + return vertices_bfs_view>(g, seed, std::allocator{}); +} + +/** + * @brief Create a BFS vertex view without value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @return vertices_bfs_view yielding vertex_info + */ +template +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_t seed_vertex) { + return vertices_bfs_view>(g, seed_vertex, std::allocator{}); +} + +/** + * @brief Create a BFS vertex view with value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @param vvf Value function invoked for each vertex + * @return vertices_bfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_id_t seed, VVF&& vvf) { + return vertices_bfs_view, std::allocator>(g, seed, std::forward(vvf), std::allocator{}); +} + +/** + * @brief Create a BFS vertex view with value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @param vvf Value function invoked for each vertex + * @return vertices_bfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_t seed_vertex, VVF&& vvf) { + return vertices_bfs_view, std::allocator>(g, seed_vertex, std::forward(vvf), std::allocator{}); +} + +/** + * @brief Create a BFS vertex view with custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @param alloc Allocator for internal containers + * @return vertices_bfs_view yielding vertex_info + */ +template + requires (!vertex_value_function>) +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_id_t seed, Alloc alloc) { + return vertices_bfs_view(g, seed, alloc); +} + +/** + * @brief Create a BFS vertex view with custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @param alloc Allocator for internal containers + * @return vertices_bfs_view yielding vertex_info + */ +template + requires (!vertex_value_function>) +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_t seed_vertex, Alloc alloc) { + return vertices_bfs_view(g, seed_vertex, alloc); +} + +/** + * @brief Create a BFS vertex view with value function and custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @param vvf Value function invoked for each vertex + * @param alloc Allocator for internal containers + * @return vertices_bfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_id_t seed, VVF&& vvf, Alloc alloc) { + return vertices_bfs_view, Alloc>(g, seed, std::forward(vvf), alloc); +} + +/** + * @brief Create a BFS vertex view with value function and custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @param vvf Value function invoked for each vertex + * @param alloc Allocator for internal containers + * @return vertices_bfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_t seed_vertex, VVF&& vvf, Alloc alloc) { + return vertices_bfs_view, Alloc>(g, seed_vertex, std::forward(vvf), alloc); +} + +//============================================================================= +// edges_bfs - BFS edge traversal +//============================================================================= + +/** + * @brief BFS edge view without value function + * + * Iterates over edges in breadth-first order yielding + * edge_info, void> + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam Alloc Allocator type for internal containers + */ +template +class edges_bfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using info_type = edge_info; + +private: + using state_type = bfs_detail::bfs_edge_state; + +public: + /** + * @brief Input iterator for BFS edge traversal + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state) + : g_(g), state_(std::move(state)) { + // Advance to first edge (seed vertex has no incoming edge to yield) + advance_to_next_edge(); + } + + [[nodiscard]] value_type operator*() const { + return value_type{current_edge_}; + } + + iterator& operator++() { + advance_to_next_edge(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance_to_next_edge(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || (state_->queue_.empty() && !has_edge_); + bool other_at_end = !other.state_ || (other.state_->queue_.empty() && !other.has_edge_); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || (state_->queue_.empty() && !has_edge_); + } + + private: + void advance_to_next_edge() { + has_edge_ = false; + if (!state_ || state_->queue_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->queue_.empty()) state_->queue_.pop(); + return; + } + + // Handle cancel_branch: mark the target of last edge to be skipped + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->skip_vertex_id_ = current_target_id_; + state_->cancel_ = cancel_search::continue_search; + } + + // Find next tree edge using BFS + while (!state_->queue_.empty()) { + auto& current = state_->queue_.front(); + + // Check if we should skip this vertex (due to cancel_branch) + auto current_vid = adj_list::vertex_id(*g_, current.vertex); + if (state_->skip_vertex_id_ && *state_->skip_vertex_id_ == current_vid) { + state_->skip_vertex_id_.reset(); + state_->queue_.pop(); + continue; + } + + // Process edges from current vertex + while (current.edge_current != current.edge_end) { + auto edge = *current.edge_current; + ++current.edge_current; + + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + + // Add target vertex to queue with its edge range + auto target_edge_range = adj_list::edges(*g_, target_v); + auto target_begin = std::ranges::begin(target_edge_range); + auto target_end = std::ranges::end(target_edge_range); + state_->queue_.push({target_v, current.depth + 1, target_end, target_begin}); + + if (current.depth + 1 > state_->max_depth_) { + state_->max_depth_ = current.depth + 1; + } + ++state_->count_; + + // Store and return this edge + current_edge_ = edge; + current_target_id_ = target_vid; + has_edge_ = true; + return; + } + } + + // No more edges from current vertex, remove it + state_->queue_.pop(); + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + edge_type current_edge_{}; + vertex_id_type current_target_id_{}; // Target vertex ID of current edge + bool has_edge_ = false; + }; + + /// Sentinel for end of BFS traversal + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr edges_bfs_view() noexcept = default; + + /// Construct from vertex descriptor + edges_bfs_view(G& g, vertex_type seed_vertex, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + edges_bfs_view(G& g, vertex_id_type seed, Alloc alloc = {}) + : edges_bfs_view(g, *adj_list::find_vertex(g, seed), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + /// Get current cancel state + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + /// Set cancel state to stop traversal + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + /// Get maximum depth reached so far + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->max_depth_ : 0; + } + + /// Get count of edges visited so far + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + std::shared_ptr state_; +}; + +/** + * @brief BFS edge view with value function + * + * Iterates over edges in breadth-first order yielding + * edge_info, EV> where EV is the invoke result of EVF. + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam EVF Edge value function type + * @tparam Alloc Allocator type for internal containers + */ +template +class edges_bfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using value_result_type = std::invoke_result_t; + using info_type = edge_info; + +private: + using state_type = bfs_detail::bfs_edge_state; + +public: + /** + * @brief Input iterator for BFS edge traversal with value function + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state, EVF* evf) + : g_(g), state_(std::move(state)), evf_(evf) { + // Advance to first edge + advance_to_next_edge(); + } + + [[nodiscard]] value_type operator*() const { + return value_type{current_edge_, std::invoke(*evf_, current_edge_)}; + } + + iterator& operator++() { + advance_to_next_edge(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance_to_next_edge(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || (state_->queue_.empty() && !has_edge_); + bool other_at_end = !other.state_ || (other.state_->queue_.empty() && !other.has_edge_); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || (state_->queue_.empty() && !has_edge_); + } + + private: + void advance_to_next_edge() { + has_edge_ = false; + if (!state_ || state_->queue_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->queue_.empty()) state_->queue_.pop(); + return; + } + + // Handle cancel_branch: mark the target of last edge to be skipped + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->skip_vertex_id_ = current_target_id_; + state_->cancel_ = cancel_search::continue_search; + } + + // Find next tree edge using BFS + while (!state_->queue_.empty()) { + auto& current = state_->queue_.front(); + + // Check if we should skip this vertex (due to cancel_branch) + auto current_vid = adj_list::vertex_id(*g_, current.vertex); + if (state_->skip_vertex_id_ && *state_->skip_vertex_id_ == current_vid) { + state_->skip_vertex_id_.reset(); + state_->queue_.pop(); + continue; + } + + // Process edges from current vertex + while (current.edge_current != current.edge_end) { + auto edge = *current.edge_current; + ++current.edge_current; + + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + + // Add target vertex to queue with its edge range + auto target_edge_range = adj_list::edges(*g_, target_v); + auto target_begin = std::ranges::begin(target_edge_range); + auto target_end = std::ranges::end(target_edge_range); + state_->queue_.push({target_v, current.depth + 1, target_end, target_begin}); + + if (current.depth + 1 > state_->max_depth_) { + state_->max_depth_ = current.depth + 1; + } + ++state_->count_; + + // Store and return this edge + current_edge_ = edge; + current_target_id_ = target_vid; + has_edge_ = true; + return; + } + } + + // No more edges from current vertex, remove it + state_->queue_.pop(); + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + EVF* evf_ = nullptr; + edge_type current_edge_{}; + vertex_id_type current_target_id_{}; // Target vertex ID of current edge + bool has_edge_ = false; + }; + + /// Sentinel for end of BFS traversal + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr edges_bfs_view() noexcept = default; + + /// Construct from vertex descriptor + edges_bfs_view(G& g, vertex_type seed_vertex, EVF evf, Alloc alloc = {}) + : g_(&g) + , evf_(std::move(evf)) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + edges_bfs_view(G& g, vertex_id_type seed, EVF evf, Alloc alloc = {}) + : edges_bfs_view(g, *adj_list::find_vertex(g, seed), std::move(evf), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_, &evf_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->max_depth_ : 0; + } + + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + [[no_unique_address]] EVF evf_; + std::shared_ptr state_; +}; + +// Deduction guides for edges_bfs_view +template +edges_bfs_view(G&, adj_list::vertex_id_t, Alloc) + -> edges_bfs_view; + +template +edges_bfs_view(G&, adj_list::vertex_t, Alloc) + -> edges_bfs_view; + +template +edges_bfs_view(G&, adj_list::vertex_id_t, EVF, Alloc) + -> edges_bfs_view; + +template +edges_bfs_view(G&, adj_list::vertex_t, EVF, Alloc) + -> edges_bfs_view; + +//============================================================================= +// Factory functions for edges_bfs +//============================================================================= + +/** + * @brief Create a BFS edge view (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @return edges_bfs_view yielding edge_info + */ +template +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_id_t seed) { + return edges_bfs_view>(g, seed); +} + +/** + * @brief Create a BFS edge view (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @return edges_bfs_view yielding edge_info + */ +template +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_t seed_vertex) { + return edges_bfs_view>(g, seed_vertex); +} + +/** + * @brief Create a BFS edge view with value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @param evf Value function invoked for each edge + * @return edges_bfs_view yielding edge_info + */ +template + requires edge_value_function> && + (!std::is_same_v, std::allocator>) +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_id_t seed, EVF&& evf) { + return edges_bfs_view, std::allocator>(g, seed, std::forward(evf)); +} + +/** + * @brief Create a BFS edge view with value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @param evf Value function invoked for each edge + * @return edges_bfs_view yielding edge_info + */ +template + requires edge_value_function> && + (!std::is_same_v, std::allocator>) +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_t seed_vertex, EVF&& evf) { + return edges_bfs_view, std::allocator>(g, seed_vertex, std::forward(evf)); +} + +/** + * @brief Create a BFS edge view with custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @param alloc Allocator for internal containers + * @return edges_bfs_view yielding edge_info + */ +template + requires (!edge_value_function>) +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_id_t seed, Alloc alloc) { + return edges_bfs_view(g, seed, alloc); +} + +/** + * @brief Create a BFS edge view with custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @param alloc Allocator for internal containers + * @return edges_bfs_view yielding edge_info + */ +template + requires (!edge_value_function>) +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_t seed_vertex, Alloc alloc) { + return edges_bfs_view(g, seed_vertex, alloc); +} + +/** + * @brief Create a BFS edge view with value function and custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for BFS + * @param evf Value function invoked for each edge + * @param alloc Allocator for internal containers + * @return edges_bfs_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_id_t seed, EVF&& evf, Alloc alloc) { + return edges_bfs_view, Alloc>(g, seed, std::forward(evf), alloc); +} + +/** + * @brief Create a BFS edge view with value function and custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for BFS + * @param evf Value function invoked for each edge + * @param alloc Allocator for internal containers + * @return edges_bfs_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_t seed_vertex, EVF&& evf, Alloc alloc) { + return edges_bfs_view, Alloc>(g, seed_vertex, std::forward(evf), alloc); +} + +} // namespace graph::views diff --git a/include/graph/views/dfs.hpp b/include/graph/views/dfs.hpp new file mode 100644 index 0000000..25461c1 --- /dev/null +++ b/include/graph/views/dfs.hpp @@ -0,0 +1,1067 @@ +/** + * @file dfs.hpp + * @brief Depth-first search views for vertices and edges + * + * Provides views that traverse a graph in depth-first order from a seed vertex, + * yielding vertex_info or edge_info for each visited element. + * + * @complexity Time: O(V + E) where V is reachable vertices and E is reachable edges + * DFS visits each reachable vertex once and traverses each reachable edge once + * @complexity Space: O(V) for the stack and visited tracker + * + * @par Examples: + * @code + * // Vertex traversal + * for (auto [v] : vertices_dfs(g, seed)) + * process_vertex(v); + * + * // Vertex traversal with value function + * for (auto [v, val] : vertices_dfs(g, seed, value_fn)) + * process_vertex_with_value(v, val); + * + * // Edge traversal + * for (auto [e] : edges_dfs(g, seed)) + * process_edge(e); + * + * // Access depth during traversal + * auto dfs = vertices_dfs(g, seed); + * for (auto [v] : dfs) + * std::cout << "Vertex " << vertex_id(g, v) << " at depth " << dfs.depth() << "\n"; + * + * // Cancel search + * auto dfs = vertices_dfs(g, seed); + * for (auto [v] : dfs) { + * if (should_stop(v)) + * dfs.cancel(cancel_search::cancel_all); + * } + * @endcode + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declarations +template > +class vertices_dfs_view; + +template > +class edges_dfs_view; + +namespace dfs_detail { + +/// Stack entry for DFS traversal: stores vertex descriptor and edge iterator +template +struct stack_entry { + Vertex vertex; + EdgeIterator edge_iter; + EdgeIterator edge_end; +}; + +/// Shared DFS state - allows iterator copies to share traversal state +/// Uses vertex descriptors internally, vertex IDs only for visited tracking +/// +/// Why shared_ptr? +/// 1. Iterator copies must share state: When you copy an iterator (e.g., auto it2 = it1), +/// both must refer to the same DFS traversal. Advancing it1 affects what it2 sees. +/// 2. View and iterators share state: The view exposes depth(), size(), and cancel() +/// accessors that reflect current traversal state modified by iterators. +/// 3. Range-based for loop: The view's cancel() must be able to stop iteration in progress. +/// 4. Input iterator semantics: DFS is single-pass; shared state correctly models this. +/// An alternative (state by value + raw pointers) would break if the view is moved. +/** + * @brief Shared DFS state for vertex traversal + * + * @complexity Time: O(V + E) - visits each reachable vertex once, traverses each reachable edge once + * @complexity Space: O(V) - stack stores up to V vertices, visited tracker uses O(V) space + */ +template +struct dfs_state { + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_iterator_type = adj_list::vertex_edge_iterator_t; + using allocator_type = Alloc; + using entry_type = stack_entry; + using stack_alloc = typename std::allocator_traits::template rebind_alloc; + + std::stack> stack_; + visited_tracker visited_; + cancel_search cancel_ = cancel_search::continue_search; + std::size_t depth_ = 0; + std::size_t count_ = 0; + + dfs_state(G& g, vertex_type seed_vertex, std::size_t num_vertices, Alloc alloc = {}) + : stack_(std::vector(alloc)) + , visited_(num_vertices, alloc) + { + auto edge_range = adj_list::edges(g, seed_vertex); + stack_.push({seed_vertex, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); + visited_.mark_visited(adj_list::vertex_id(g, seed_vertex)); + // Note: count_ is not incremented here. It's incremented in advance() when + // a vertex is actually yielded by the iterator. + } +}; + +} // namespace dfs_detail + +/** + * @brief DFS vertex view without value function + * + * Iterates over vertices in depth-first order yielding + * vertex_info, void> + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam Alloc Allocator type for internal containers + */ +template +class vertices_dfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using info_type = vertex_info; + +private: + using state_type = dfs_detail::dfs_state; + +public: + /** + * @brief Input iterator for DFS vertex traversal + * + * Note: This is an input iterator because DFS state is shared and + * advancing one iterator affects all copies. + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state) + : g_(g), state_(std::move(state)) {} + + [[nodiscard]] value_type operator*() const { + return value_type{state_->stack_.top().vertex}; + } + + iterator& operator++() { + advance(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + // Both at end if state is null or stack is empty + bool this_at_end = !state_ || state_->stack_.empty(); + bool other_at_end = !other.state_ || other.state_->stack_.empty(); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || state_->stack_.empty(); + } + + private: + void advance() { + if (!state_ || state_->stack_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->stack_.empty()) state_->stack_.pop(); + return; + } + + // Handle cancel_branch: skip current subtree, continue with siblings + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + state_->cancel_ = cancel_search::continue_search; + } + + // Find next unvisited vertex using DFS + while (!state_->stack_.empty()) { + auto& top = state_->stack_.top(); + + // Find next unvisited neighbor from current vertex + bool found_unvisited = false; + while (top.edge_iter != top.edge_end) { + auto edge = *top.edge_iter; + ++top.edge_iter; + + // Get target vertex descriptor using CPO + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + + // Push target vertex with its edge range + auto edge_range = adj_list::edges(*g_, target_v); + state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); + ++state_->depth_; + ++state_->count_; + found_unvisited = true; + break; + } + } + + if (found_unvisited) { + return; // Found next vertex to visit + } + + // No more unvisited neighbors, backtrack + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + }; + + /// Sentinel for end of DFS traversal + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr vertices_dfs_view() noexcept = default; + + /// Construct from vertex descriptor + vertices_dfs_view(G& g, vertex_type seed_vertex, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + vertices_dfs_view(G& g, vertex_id_type seed, Alloc alloc = {}) + : vertices_dfs_view(g, *adj_list::find_vertex(g, seed), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + /// Get current cancel state + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + /// Set cancel state to stop traversal + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + /// Get current depth in DFS tree (stack size) + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->stack_.size() : 0; + } + + /// Get count of vertices visited so far + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + std::shared_ptr state_; +}; + +/** + * @brief DFS vertex view with value function + * + * Iterates over vertices in depth-first order yielding + * vertex_info, VV> where VV is the invoke result of VVF. + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam VVF Vertex value function type + * @tparam Alloc Allocator type for internal containers + */ +template +class vertices_dfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using value_result_type = std::invoke_result_t; + using info_type = vertex_info; + +private: + using state_type = dfs_detail::dfs_state; + +public: + /** + * @brief Input iterator for DFS vertex traversal with value function + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state, VVF* vvf) + : g_(g), state_(std::move(state)), vvf_(vvf) {} + + [[nodiscard]] value_type operator*() const { + auto v = state_->stack_.top().vertex; + return value_type{v, std::invoke(*vvf_, v)}; + } + + iterator& operator++() { + advance(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || state_->stack_.empty(); + bool other_at_end = !other.state_ || other.state_->stack_.empty(); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || state_->stack_.empty(); + } + + private: + void advance() { + if (!state_ || state_->stack_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->stack_.empty()) state_->stack_.pop(); + return; + } + + // Handle cancel_branch: skip current subtree, continue with siblings + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + state_->cancel_ = cancel_search::continue_search; + } + + // Find next unvisited vertex using DFS + while (!state_->stack_.empty()) { + auto& top = state_->stack_.top(); + + // Find next unvisited neighbor from current vertex + bool found_unvisited = false; + while (top.edge_iter != top.edge_end) { + auto edge = *top.edge_iter; + ++top.edge_iter; + + // Get target vertex descriptor using CPO + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + + // Push target vertex with its edge range + auto edge_range = adj_list::edges(*g_, target_v); + state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); + ++state_->depth_; + ++state_->count_; + found_unvisited = true; + break; + } + } + + if (found_unvisited) { + return; // Found next vertex to visit + } + + // No more unvisited neighbors, backtrack + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + VVF* vvf_ = nullptr; + }; + + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr vertices_dfs_view() noexcept = default; + + /// Construct from vertex descriptor + vertices_dfs_view(G& g, vertex_type seed_vertex, VVF vvf, Alloc alloc = {}) + : g_(&g) + , vvf_(std::move(vvf)) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + vertices_dfs_view(G& g, vertex_id_type seed, VVF vvf, Alloc alloc = {}) + : vertices_dfs_view(g, *adj_list::find_vertex(g, seed), std::move(vvf), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_, &vvf_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->stack_.size() : 0; + } + + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + [[no_unique_address]] VVF vvf_{}; + std::shared_ptr state_; +}; + +// Deduction guides for vertex_id +template > +vertices_dfs_view(G&, adj_list::vertex_id_t, Alloc) -> vertices_dfs_view; + +template +vertices_dfs_view(G&, adj_list::vertex_id_t) -> vertices_dfs_view>; + +template > +vertices_dfs_view(G&, adj_list::vertex_id_t, VVF, Alloc) -> vertices_dfs_view; + +// Deduction guides for vertex descriptor +template > +vertices_dfs_view(G&, adj_list::vertex_t, Alloc) -> vertices_dfs_view; + +template +vertices_dfs_view(G&, adj_list::vertex_t) -> vertices_dfs_view>; + +template > +vertices_dfs_view(G&, adj_list::vertex_t, VVF, Alloc) -> vertices_dfs_view; + +/** + * @brief Create a DFS vertex view without value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @return vertices_dfs_view yielding vertex_info + */ +template +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_id_t seed) { + return vertices_dfs_view>(g, seed, std::allocator{}); +} + +/** + * @brief Create a DFS vertex view without value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @return vertices_dfs_view yielding vertex_info + */ +template +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_t seed_vertex) { + return vertices_dfs_view>(g, seed_vertex, std::allocator{}); +} + +/** + * @brief Create a DFS vertex view with value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @param vvf Value function invoked for each vertex + * @return vertices_dfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_id_t seed, VVF&& vvf) { + return vertices_dfs_view, std::allocator>(g, seed, std::forward(vvf), std::allocator{}); +} + +/** + * @brief Create a DFS vertex view with value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @param vvf Value function invoked for each vertex + * @return vertices_dfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_t seed_vertex, VVF&& vvf) { + return vertices_dfs_view, std::allocator>(g, seed_vertex, std::forward(vvf), std::allocator{}); +} + +/** + * @brief Create a DFS vertex view with custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @param alloc Allocator for internal containers + * @return vertices_dfs_view yielding vertex_info + */ +template + requires (!vertex_value_function>) +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_id_t seed, Alloc alloc) { + return vertices_dfs_view(g, seed, alloc); +} + +/** + * @brief Create a DFS vertex view with custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @param alloc Allocator for internal containers + * @return vertices_dfs_view yielding vertex_info + */ +template + requires (!vertex_value_function>) +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_t seed_vertex, Alloc alloc) { + return vertices_dfs_view(g, seed_vertex, alloc); +} + +/** + * @brief Create a DFS vertex view with value function and custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @param vvf Value function invoked for each vertex + * @param alloc Allocator for internal containers + * @return vertices_dfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_id_t seed, VVF&& vvf, Alloc alloc) { + return vertices_dfs_view, Alloc>(g, seed, std::forward(vvf), alloc); +} + +/** + * @brief Create a DFS vertex view with value function and custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @param vvf Value function invoked for each vertex + * @param alloc Allocator for internal containers + * @return vertices_dfs_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_t seed_vertex, VVF&& vvf, Alloc alloc) { + return vertices_dfs_view, Alloc>(g, seed_vertex, std::forward(vvf), alloc); +} + +// ============================================================================= +// edges_dfs_view - DFS edge traversal +// ============================================================================= + +/** + * @brief DFS edge view without value function + * + * Iterates over edges in depth-first order yielding + * edge_info, void> + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam Alloc Allocator type for internal containers + */ +template +class edges_dfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using info_type = edge_info; + +private: + using state_type = dfs_detail::dfs_state; + +public: + /** + * @brief Input iterator for DFS edge traversal + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state) + : g_(g), state_(std::move(state)) { + // Advance to first edge (seed vertex has no incoming edge to yield) + advance_to_next_edge(); + } + + [[nodiscard]] value_type operator*() const { + return value_type{current_edge_}; + } + + iterator& operator++() { + advance_to_next_edge(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance_to_next_edge(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || (state_->stack_.empty() && !has_edge_); + bool other_at_end = !other.state_ || (other.state_->stack_.empty() && !other.has_edge_); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || (state_->stack_.empty() && !has_edge_); + } + + private: + void advance_to_next_edge() { + has_edge_ = false; + if (!state_ || state_->stack_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->stack_.empty()) state_->stack_.pop(); + return; + } + + // Handle cancel_branch: skip current subtree, continue with siblings + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + state_->cancel_ = cancel_search::continue_search; + } + + // Find next tree edge using DFS + while (!state_->stack_.empty()) { + auto& top = state_->stack_.top(); + + // Find next unvisited neighbor from current vertex + while (top.edge_iter != top.edge_end) { + auto edge = *top.edge_iter; + ++top.edge_iter; + + // Get target vertex descriptor using CPO + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + + // Push target vertex with its edge range + auto edge_range = adj_list::edges(*g_, target_v); + state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); + ++state_->depth_; + ++state_->count_; + + // Store current edge and return + current_edge_ = edge; + has_edge_ = true; + return; + } + } + + // No more unvisited neighbors, backtrack + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + edge_type current_edge_{}; + bool has_edge_ = false; + }; + + /// Sentinel for end of DFS traversal + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr edges_dfs_view() noexcept = default; + + /// Construct from vertex descriptor + edges_dfs_view(G& g, vertex_type seed_vertex, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + edges_dfs_view(G& g, vertex_id_type seed, Alloc alloc = {}) + : edges_dfs_view(g, *adj_list::find_vertex(g, seed), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + /// Get current cancel state + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + /// Set cancel state to stop traversal + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + /// Get current depth in DFS tree (stack size) + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->stack_.size() : 0; + } + + /// Get count of edges visited so far + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + std::shared_ptr state_; +}; + +/** + * @brief DFS edge view with value function + * + * Iterates over edges in depth-first order yielding + * edge_info, EV> where EV is the invoke result of EVF. + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam EVF Edge value function type + * @tparam Alloc Allocator type for internal containers + */ +template +class edges_dfs_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using value_result_type = std::invoke_result_t; + using info_type = edge_info; + +private: + using state_type = dfs_detail::dfs_state; + +public: + /** + * @brief Input iterator for DFS edge traversal with value function + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state, EVF* evf) + : g_(g), state_(std::move(state)), evf_(evf) { + advance_to_next_edge(); + } + + [[nodiscard]] value_type operator*() const { + return value_type{current_edge_, std::invoke(*evf_, current_edge_)}; + } + + iterator& operator++() { + advance_to_next_edge(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + advance_to_next_edge(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + bool this_at_end = !state_ || (state_->stack_.empty() && !has_edge_); + bool other_at_end = !other.state_ || (other.state_->stack_.empty() && !other.has_edge_); + return this_at_end == other_at_end; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || (state_->stack_.empty() && !has_edge_); + } + + private: + void advance_to_next_edge() { + has_edge_ = false; + if (!state_ || state_->stack_.empty()) return; + if (state_->cancel_ == cancel_search::cancel_all) { + while (!state_->stack_.empty()) state_->stack_.pop(); + return; + } + + // Handle cancel_branch: skip current subtree, continue with siblings + if (state_->cancel_ == cancel_search::cancel_branch) { + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + state_->cancel_ = cancel_search::continue_search; + } + + while (!state_->stack_.empty()) { + auto& top = state_->stack_.top(); + + while (top.edge_iter != top.edge_end) { + auto edge = *top.edge_iter; + ++top.edge_iter; + + auto target_v = adj_list::target(*g_, edge); + auto target_vid = adj_list::vertex_id(*g_, target_v); + + if (!state_->visited_.is_visited(target_vid)) { + state_->visited_.mark_visited(target_vid); + + auto edge_range = adj_list::edges(*g_, target_v); + state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); + ++state_->depth_; + ++state_->count_; + + current_edge_ = edge; + has_edge_ = true; + return; + } + } + + state_->stack_.pop(); + if (state_->depth_ > 0) --state_->depth_; + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + EVF* evf_ = nullptr; + edge_type current_edge_{}; + bool has_edge_ = false; + }; + + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr edges_dfs_view() noexcept = default; + + /// Construct from vertex descriptor + edges_dfs_view(G& g, vertex_type seed_vertex, EVF evf, Alloc alloc = {}) + : g_(&g) + , evf_(std::move(evf)) + , state_(std::make_shared(g, seed_vertex, adj_list::num_vertices(g), alloc)) + {} + + /// Construct from vertex ID (delegates to vertex descriptor constructor) + edges_dfs_view(G& g, vertex_id_type seed, EVF evf, Alloc alloc = {}) + : edges_dfs_view(g, *adj_list::find_vertex(g, seed), std::move(evf), alloc) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_, &evf_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + [[nodiscard]] cancel_search cancel() const noexcept { + return state_ ? state_->cancel_ : cancel_search::continue_search; + } + + void cancel(cancel_search c) noexcept { + if (state_) state_->cancel_ = c; + } + + [[nodiscard]] std::size_t depth() const noexcept { + return state_ ? state_->stack_.size() : 0; + } + + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->count_ : 0; + } + +private: + G* g_ = nullptr; + [[no_unique_address]] EVF evf_{}; + std::shared_ptr state_; +}; + +// Deduction guides for edges_dfs_view - vertex_id +template > +edges_dfs_view(G&, adj_list::vertex_id_t, Alloc) -> edges_dfs_view; + +template +edges_dfs_view(G&, adj_list::vertex_id_t) -> edges_dfs_view>; + +template > +edges_dfs_view(G&, adj_list::vertex_id_t, EVF, Alloc) -> edges_dfs_view; + +// Deduction guides for edges_dfs_view - vertex descriptor +template > +edges_dfs_view(G&, adj_list::vertex_t, Alloc) -> edges_dfs_view; + +template +edges_dfs_view(G&, adj_list::vertex_t) -> edges_dfs_view>; + +template > +edges_dfs_view(G&, adj_list::vertex_t, EVF, Alloc) -> edges_dfs_view; + +// ============================================================================= +// edges_dfs factory functions +// ============================================================================= + +/** + * @brief Create a DFS edge view without value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @return edges_dfs_view yielding edge_info + */ +template +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_id_t seed) { + return edges_dfs_view>(g, seed, std::allocator{}); +} + +/** + * @brief Create a DFS edge view without value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @return edges_dfs_view yielding edge_info + */ +template +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_t seed_vertex) { + return edges_dfs_view>(g, seed_vertex, std::allocator{}); +} + +/** + * @brief Create a DFS edge view with value function (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @param evf Value function invoked for each edge + * @return edges_dfs_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_id_t seed, EVF&& evf) { + return edges_dfs_view, std::allocator>(g, seed, std::forward(evf), std::allocator{}); +} + +/** + * @brief Create a DFS edge view with value function (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @param evf Value function invoked for each edge + * @return edges_dfs_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_t seed_vertex, EVF&& evf) { + return edges_dfs_view, std::allocator>(g, seed_vertex, std::forward(evf), std::allocator{}); +} + +/** + * @brief Create a DFS edge view with custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @param alloc Allocator for internal containers + * @return edges_dfs_view yielding edge_info + */ +template + requires (!edge_value_function>) +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_id_t seed, Alloc alloc) { + return edges_dfs_view(g, seed, alloc); +} + +/** + * @brief Create a DFS edge view with custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @param alloc Allocator for internal containers + * @return edges_dfs_view yielding edge_info + */ +template + requires (!edge_value_function>) +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_t seed_vertex, Alloc alloc) { + return edges_dfs_view(g, seed_vertex, alloc); +} + +/** + * @brief Create a DFS edge view with value function and custom allocator (from vertex ID) + * + * @param g The graph to traverse + * @param seed Starting vertex ID for DFS + * @param evf Value function invoked for each edge + * @param alloc Allocator for internal containers + * @return edges_dfs_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_id_t seed, EVF&& evf, Alloc alloc) { + return edges_dfs_view, Alloc>(g, seed, std::forward(evf), alloc); +} + +/** + * @brief Create a DFS edge view with value function and custom allocator (from vertex descriptor) + * + * @param g The graph to traverse + * @param seed_vertex Starting vertex descriptor for DFS + * @param evf Value function invoked for each edge + * @param alloc Allocator for internal containers + * @return edges_dfs_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_t seed_vertex, EVF&& evf, Alloc alloc) { + return edges_dfs_view, Alloc>(g, seed_vertex, std::forward(evf), alloc); +} + +} // namespace graph::views diff --git a/include/graph/views/edgelist.hpp b/include/graph/views/edgelist.hpp new file mode 100644 index 0000000..5250a00 --- /dev/null +++ b/include/graph/views/edgelist.hpp @@ -0,0 +1,520 @@ +/** + * @file edgelist.hpp + * @brief Edgelist view for iterating over all edges in a graph + * + * Provides a view that flattens the two-level adjacency list structure into + * a single range of edges, yielding edge_info, EV> + * for each edge. Each edge descriptor provides access to both source and + * target vertices. Supports optional value functions to compute per-edge values. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declaration +template +class edgelist_view; + +/** + * @brief Edgelist view without value function + * + * Iterates over all edges in the graph yielding edge_info, void> + * + * @tparam G Graph type satisfying adjacency_list concept + */ +template +class edgelist_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_range_type = adj_list::vertex_edge_range_t; + using edge_type = adj_list::edge_t; + using info_type = edge_info; + + /** + * @brief Forward iterator that flattens vertex-edge structure + * + * Iterates through all vertices, and for each vertex, iterates through + * all of its edges, presenting a single flat sequence of edges. + * Uses vertex and edge descriptors directly, similar to vertexlist_view and incidence_view. + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = const value_type&; + + constexpr iterator() noexcept = default; + + constexpr iterator(G* g, vertex_type v, vertex_type v_end, + edge_type current_edge, edge_type edge_end) noexcept + : g_(g), v_(v), v_end_(v_end), current_{current_edge}, edge_end_(edge_end) {} + + [[nodiscard]] constexpr reference operator*() const noexcept { + return current_; + } + + constexpr iterator& operator++() noexcept { + ++current_.edge; + // If we've exhausted edges for current vertex, move to next vertex + if (current_.edge == edge_end_) { + ++v_; + advance_to_valid_edge(); + } + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++*this; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + // Two iterators are equal if: + // 1. Both are at the end (v_ == v_end_), or + // 2. They point to the same vertex and edge + if (v_ == v_end_ && other.v_ == other.v_end_) { + return true; + } + if (v_ == v_end_ || other.v_ == other.v_end_) { + return false; + } + return v_ == other.v_ && current_.edge == other.current_.edge; + } + + /** + * @brief Advance to the next vertex that has edges, initializing edge descriptors + * + * Skips over vertices with no edges until we find one with edges or reach the end. + */ + constexpr void advance_to_valid_edge() noexcept { + while (v_ != v_end_) { + auto edge_range = adj_list::edges(*g_, v_); + auto begin_it = std::ranges::begin(edge_range); + auto end_it = std::ranges::end(edge_range); + if (begin_it != end_it) { + current_ = info_type{*begin_it}; + edge_end_ = *end_it; + return; // Found a vertex with edges + } + ++v_; + } + // Reached end - no more vertices with edges + } + + private: + G* g_ = nullptr; + vertex_type v_{}; // Current vertex descriptor + vertex_type v_end_{}; // End sentinel for vertices + value_type current_{}; // Stores edge_info with edge descriptor + edge_type edge_end_{}; // End sentinel for current vertex's edges + }; + + using const_iterator = iterator; + + constexpr edgelist_view() noexcept = default; + + constexpr edgelist_view(G& g) noexcept + : g_(&g) {} + + [[nodiscard]] constexpr iterator begin() const noexcept { + auto v_range = adj_list::vertices(*g_); + auto v_begin = *std::ranges::begin(v_range); + auto v_end = *std::ranges::end(v_range); + + // Create iterator and advance to first valid edge + iterator it(g_, v_begin, v_end, edge_type{}, edge_type{}); + it.advance_to_valid_edge(); + return it; + } + + [[nodiscard]] constexpr iterator end() const noexcept { + auto v_range = adj_list::vertices(*g_); + auto v_end = *std::ranges::end(v_range); + return iterator(g_, v_end, v_end, edge_type{}, edge_type{}); + } + +private: + G* g_ = nullptr; +}; + +/** + * @brief Edgelist view with value function + * + * Iterates over all edges yielding edge_info, EV> + * where EV is the result of invoking the value function on the edge descriptor. + * + * @tparam G Graph type satisfying adjacency_list concept + * @tparam EVF Edge value function type + */ +template +class edgelist_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_range_type = adj_list::vertex_edge_range_t; + using edge_type = adj_list::edge_t; + using value_type_result = std::invoke_result_t; + using info_type = edge_info; + + /** + * @brief Forward iterator that flattens vertex-edge structure with value computation + * + * Uses vertex and edge descriptors directly, similar to vertexlist_view and incidence_view. + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(G* g, vertex_type v, vertex_type v_end, + edge_type current_edge, edge_type edge_end, EVF* evf) noexcept + : g_(g), v_(v), v_end_(v_end), current_edge_(current_edge), + edge_end_(edge_end), evf_(evf) {} + + [[nodiscard]] constexpr value_type operator*() const { + return value_type{current_edge_, std::invoke(*evf_, current_edge_)}; + } + + constexpr iterator& operator++() noexcept { + ++current_edge_; + if (current_edge_ == edge_end_) { + ++v_; + advance_to_valid_edge(); + } + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++*this; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + if (v_ == v_end_ && other.v_ == other.v_end_) { + return true; + } + if (v_ == v_end_ || other.v_ == other.v_end_) { + return false; + } + return v_ == other.v_ && current_edge_ == other.current_edge_; + } + + /** + * @brief Advance to the next vertex that has edges, initializing edge descriptors + */ + constexpr void advance_to_valid_edge() noexcept { + while (v_ != v_end_) { + auto edge_range = adj_list::edges(*g_, v_); + auto begin_it = std::ranges::begin(edge_range); + auto end_it = std::ranges::end(edge_range); + if (begin_it != end_it) { + current_edge_ = *begin_it; + edge_end_ = *end_it; + return; + } + ++v_; + } + } + + private: + G* g_ = nullptr; + vertex_type v_{}; // Current vertex descriptor + vertex_type v_end_{}; // End sentinel for vertices + edge_type current_edge_{}; // Current edge descriptor + edge_type edge_end_{}; // End sentinel for current vertex's edges + EVF* evf_ = nullptr; + }; + + using const_iterator = iterator; + + constexpr edgelist_view() noexcept = default; + + constexpr edgelist_view(G& g, EVF evf) noexcept(std::is_nothrow_move_constructible_v) + : g_(&g), evf_(std::move(evf)) {} + + [[nodiscard]] constexpr iterator begin() noexcept { + auto v_range = adj_list::vertices(*g_); + auto v_begin = *std::ranges::begin(v_range); + auto v_end = *std::ranges::end(v_range); + + iterator it(g_, v_begin, v_end, edge_type{}, edge_type{}, &evf_); + it.advance_to_valid_edge(); + return it; + } + + [[nodiscard]] constexpr iterator end() noexcept { + auto v_range = adj_list::vertices(*g_); + auto v_end = *std::ranges::end(v_range); + return iterator(g_, v_end, v_end, edge_type{}, edge_type{}, &evf_); + } + +private: + G* g_ = nullptr; + [[no_unique_address]] EVF evf_{}; +}; + +// ============================================================================= +// Factory Functions +// ============================================================================= + +/** + * @brief Create an edgelist view over all edges in an adjacency list + * + * @tparam G Graph type satisfying adjacency_list concept + * @param g The graph to iterate over + * @return edgelist_view yielding edge_info, void> + */ +template +[[nodiscard]] constexpr auto edgelist(G& g) noexcept { + return edgelist_view(g); +} + +/** + * @brief Create an edgelist view with value function over all edges + * + * @tparam G Graph type satisfying adjacency_list concept + * @tparam EVF Edge value function type + * @param g The graph to iterate over + * @param evf Function to compute values from edge descriptors + * @return edgelist_view yielding edge_info, EV> + */ +template + requires edge_value_function> +[[nodiscard]] constexpr auto edgelist(G& g, EVF&& evf) + noexcept(std::is_nothrow_constructible_v, EVF>) +{ + return edgelist_view>(g, std::forward(evf)); +} + +// ============================================================================= +// Edge List Views (for edge_list data structures) +// ============================================================================= + +// Forward declaration +template +class edge_list_edgelist_view; + +/** + * @brief Edgelist view for edge_list without value function + * + * Wraps an edge_list range directly, yielding edge_info, void> + * + * @tparam EL Edge list type satisfying basic_sourced_edgelist concept + */ +template +class edge_list_edgelist_view : public std::ranges::view_interface> { +public: + using edge_list_type = EL; + using edge_type = edge_list::edge_t; + using info_type = edge_info; + + /** + * @brief Forward iterator wrapping edge_list iteration + */ + class iterator { + public: + using base_iterator = std::ranges::iterator_t; + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(base_iterator it) noexcept + : current_(it) {} + + [[nodiscard]] constexpr value_type operator*() const { + return value_type{*current_}; + } + + constexpr iterator& operator++() noexcept { + ++current_; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_ == other.current_; + } + + private: + base_iterator current_{}; + }; + + using const_iterator = iterator; + + constexpr edge_list_edgelist_view() noexcept = default; + + constexpr edge_list_edgelist_view(EL& el) noexcept + : el_(&el) {} + + [[nodiscard]] constexpr iterator begin() const noexcept { + return iterator(std::ranges::begin(*el_)); + } + + [[nodiscard]] constexpr iterator end() const noexcept { + return iterator(std::ranges::end(*el_)); + } + + [[nodiscard]] constexpr auto size() const noexcept + requires std::ranges::sized_range + { + return std::ranges::size(*el_); + } + +private: + EL* el_ = nullptr; +}; + +/** + * @brief Edgelist view for edge_list with value function + * + * Wraps an edge_list range, yielding edge_info, EV> + * where EV is the result of invoking the value function on the edge. + * + * @tparam EL Edge list type satisfying basic_sourced_edgelist concept + * @tparam EVF Edge value function type + */ +template +class edge_list_edgelist_view : public std::ranges::view_interface> { +public: + using edge_list_type = EL; + using edge_type = edge_list::edge_t; + using value_type_result = std::invoke_result_t; + using info_type = edge_info; + + /** + * @brief Forward iterator wrapping edge_list iteration with value computation + */ + class iterator { + public: + using base_iterator = std::ranges::iterator_t; + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(EL* el, base_iterator it, EVF* evf) noexcept + : el_(el), current_(it), evf_(evf) {} + + [[nodiscard]] constexpr value_type operator*() const { + auto& edge = *current_; + return value_type{edge, std::invoke(*evf_, *el_, edge)}; + } + + constexpr iterator& operator++() noexcept { + ++current_; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_ == other.current_; + } + + private: + EL* el_ = nullptr; + base_iterator current_{}; + EVF* evf_ = nullptr; + }; + + using const_iterator = iterator; + + constexpr edge_list_edgelist_view() noexcept = default; + + constexpr edge_list_edgelist_view(EL& el, EVF evf) noexcept(std::is_nothrow_move_constructible_v) + : el_(&el), evf_(std::move(evf)) {} + + [[nodiscard]] constexpr iterator begin() noexcept { + return iterator(el_, std::ranges::begin(*el_), &evf_); + } + + [[nodiscard]] constexpr iterator end() noexcept { + return iterator(el_, std::ranges::end(*el_), &evf_); + } + + [[nodiscard]] constexpr auto size() const noexcept + requires std::ranges::sized_range + { + return std::ranges::size(*el_); + } + +private: + EL* el_ = nullptr; + [[no_unique_address]] EVF evf_{}; +}; + +// ============================================================================= +// Factory Functions for edge_list +// ============================================================================= + +/** + * @brief Create an edgelist view over an edge_list + * + * @tparam EL Edge list type satisfying basic_sourced_edgelist concept + * @param el The edge list to iterate over + * @return edge_list_edgelist_view yielding edge_info, void> + */ +template + requires (!adj_list::adjacency_list) // Disambiguation: prefer adjacency_list overload +[[nodiscard]] constexpr auto edgelist(EL& el) noexcept { + return edge_list_edgelist_view(el); +} + +/** + * @brief Create an edgelist view with value function over an edge_list + * + * @tparam EL Edge list type satisfying basic_sourced_edgelist concept + * @tparam EVF Edge value function type (receives edge_list& and edge) + * @param el The edge list to iterate over + * @param evf Function to compute values from edges + * @return edge_list_edgelist_view yielding edge_info, EV> + */ +template + requires (!adj_list::adjacency_list) && // Disambiguation: prefer adjacency_list overload + std::invocable> +[[nodiscard]] constexpr auto edgelist(EL& el, EVF&& evf) + noexcept(std::is_nothrow_constructible_v, EVF>) +{ + return edge_list_edgelist_view>(el, std::forward(evf)); +} + +} // namespace graph::views diff --git a/include/graph/views/incidence.hpp b/include/graph/views/incidence.hpp new file mode 100644 index 0000000..bf9827c --- /dev/null +++ b/include/graph/views/incidence.hpp @@ -0,0 +1,265 @@ +/** + * @file incidence.hpp + * @brief Incidence view for iterating over edges from a vertex + * + * Provides a view that iterates over all outgoing edges from a given vertex, + * yielding edge_info, EV> for each edge. Supports + * optional value functions to compute per-edge values. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declaration +template +class incidence_view; + +/** + * @brief Incidence view without value function + * + * Iterates over edges from a vertex yielding edge_info, void> + * + * @tparam G Graph type satisfying adjacency_list concept + */ +template +class incidence_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_range_type = adj_list::vertex_edge_range_t; + using edge_iterator_type = adj_list::vertex_edge_iterator_t; + using edge_type = adj_list::edge_t; + using info_type = edge_info; + + /** + * @brief Forward iterator yielding edge_info values + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = const value_type&; + + constexpr iterator() noexcept = default; + + constexpr iterator(edge_type e) noexcept + : current_{e} {} + + [[nodiscard]] constexpr reference operator*() const noexcept { + return current_; + } + + constexpr iterator& operator++() noexcept { + ++current_.edge; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_.edge; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_.edge == other.current_.edge; + } + + private: + value_type current_{}; + }; + + using const_iterator = iterator; + + constexpr incidence_view() noexcept = default; + + constexpr incidence_view(G& g, vertex_type u) noexcept + : g_(&g), source_(u) {} + + [[nodiscard]] constexpr iterator begin() const noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(*std::ranges::begin(edge_range)); + } + + [[nodiscard]] constexpr iterator end() const noexcept { + auto edge_range = adj_list::edges(*g_, source_); + // edge_descriptor_view's end iterator can be dereferenced to get + // an edge_descriptor with the end storage position + return iterator(*std::ranges::end(edge_range)); + } + + [[nodiscard]] constexpr auto size() const noexcept + requires std::ranges::sized_range + { + return std::ranges::size(adj_list::edges(*g_, source_)); + } + +private: + G* g_ = nullptr; + vertex_type source_{}; +}; + +/** + * @brief Incidence view with value function + * + * Iterates over edges from a vertex yielding edge_info, EV> + * where EV is the result of invoking the value function on the edge descriptor. + * + * @tparam G Graph type satisfying adjacency_list concept + * @tparam EVF Edge value function type + */ +template +class incidence_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_range_type = adj_list::vertex_edge_range_t; + using edge_iterator_type = adj_list::vertex_edge_iterator_t; + using edge_type = adj_list::edge_t; + using value_type_result = std::invoke_result_t; + using info_type = edge_info; + + /** + * @brief Forward iterator yielding edge_info values with computed value + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(edge_type e, EVF* evf) noexcept + : current_(e), evf_(evf) {} + + [[nodiscard]] constexpr value_type operator*() const { + return value_type{current_, std::invoke(*evf_, current_)}; + } + + constexpr iterator& operator++() noexcept { + ++current_; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_ == other.current_; + } + + private: + edge_type current_{}; + EVF* evf_ = nullptr; + }; + + using const_iterator = iterator; + + constexpr incidence_view() noexcept = default; + + constexpr incidence_view(G& g, vertex_type u, EVF evf) noexcept(std::is_nothrow_move_constructible_v) + : g_(&g), source_(u), evf_(std::move(evf)) {} + + [[nodiscard]] constexpr iterator begin() noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(*std::ranges::begin(edge_range), &evf_); + } + + [[nodiscard]] constexpr iterator end() noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(*std::ranges::end(edge_range), &evf_); + } + + [[nodiscard]] constexpr auto size() const noexcept + requires std::ranges::sized_range + { + return std::ranges::size(adj_list::edges(*g_, source_)); + } + +private: + G* g_ = nullptr; + vertex_type source_{}; + [[no_unique_address]] EVF evf_{}; +}; + +// Deduction guides +template +incidence_view(G&, adj_list::vertex_t) -> incidence_view; + +template +incidence_view(G&, adj_list::vertex_t, EVF) -> incidence_view; + +/** + * @brief Create an incidence view without value function + * + * @param g The graph to iterate over + * @param u The source vertex descriptor + * @return incidence_view yielding edge_info + */ +template +[[nodiscard]] constexpr auto incidence(G& g, adj_list::vertex_t u) noexcept { + return incidence_view(g, u); +} + +/** + * @brief Create an incidence view with value function + * + * @param g The graph to iterate over + * @param u The source vertex descriptor + * @param evf Value function invoked for each edge + * @return incidence_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] constexpr auto incidence(G& g, adj_list::vertex_t u, EVF&& evf) { + return incidence_view>(g, u, std::forward(evf)); +} + +/** + * @brief Create an incidence view using vertex id (convenience overload) + * + * @param g The graph to iterate over + * @param uid The source vertex id + * @return incidence_view yielding edge_info + */ +template +[[nodiscard]] constexpr auto incidence(G& g, adj_list::vertex_id_t uid) noexcept { + auto u = *adj_list::find_vertex(g, uid); + return incidence_view(g, u); +} + +/** + * @brief Create an incidence view with value function using vertex id (convenience overload) + * + * @param g The graph to iterate over + * @param uid The source vertex id + * @param evf Value function invoked for each edge + * @return incidence_view yielding edge_info + */ +template + requires edge_value_function> +[[nodiscard]] constexpr auto incidence(G& g, adj_list::vertex_id_t uid, EVF&& evf) { + auto u = *adj_list::find_vertex(g, uid); + return incidence_view>(g, u, std::forward(evf)); +} + +} // namespace graph::views diff --git a/include/graph/views/neighbors.hpp b/include/graph/views/neighbors.hpp new file mode 100644 index 0000000..ca9e5ca --- /dev/null +++ b/include/graph/views/neighbors.hpp @@ -0,0 +1,268 @@ +/** + * @file neighbors.hpp + * @brief Neighbors view for iterating over adjacent vertices + * + * Provides a view that iterates over all neighbor vertices reachable from + * a given vertex, yielding neighbor_info, VV> for + * each neighbor. Supports optional value functions to compute per-neighbor values. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declaration +template +class neighbors_view; + +/** + * @brief Neighbors view without value function + * + * Iterates over neighbors yielding neighbor_info, void> + * + * @tparam G Graph type satisfying adjacency_list concept + */ +template +class neighbors_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_range_type = adj_list::vertex_edge_range_t; + using edge_iterator_type = adj_list::vertex_edge_iterator_t; + using edge_type = adj_list::edge_t; + using info_type = neighbor_info; + + /** + * @brief Forward iterator yielding neighbor_info values + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(G* g, edge_type e) noexcept + : g_(g), current_edge_(e) {} + + [[nodiscard]] constexpr value_type operator*() const noexcept { + // Get target vertex descriptor from the edge + auto target = adj_list::target(*g_, current_edge_); + return value_type{target}; + } + + constexpr iterator& operator++() noexcept { + ++current_edge_; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_edge_; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_edge_ == other.current_edge_; + } + + private: + G* g_ = nullptr; + edge_type current_edge_{}; + }; + + using const_iterator = iterator; + + constexpr neighbors_view() noexcept = default; + + constexpr neighbors_view(G& g, vertex_type u) noexcept + : g_(&g), source_(u) {} + + [[nodiscard]] constexpr iterator begin() const noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(g_, *std::ranges::begin(edge_range)); + } + + [[nodiscard]] constexpr iterator end() const noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(g_, *std::ranges::end(edge_range)); + } + + [[nodiscard]] constexpr auto size() const noexcept + requires std::ranges::sized_range + { + return std::ranges::size(adj_list::edges(*g_, source_)); + } + +private: + G* g_ = nullptr; + vertex_type source_{}; +}; + +/** + * @brief Neighbors view with value function + * + * Iterates over neighbors yielding neighbor_info, VV> + * where VV is the result of invoking the value function on the target vertex descriptor. + * + * @tparam G Graph type satisfying adjacency_list concept + * @tparam VVF Vertex value function type + */ +template +class neighbors_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using edge_range_type = adj_list::vertex_edge_range_t; + using edge_iterator_type = adj_list::vertex_edge_iterator_t; + using edge_type = adj_list::edge_t; + using value_type_result = std::invoke_result_t; + using info_type = neighbor_info; + + /** + * @brief Forward iterator yielding neighbor_info values with computed value + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(G* g, edge_type e, VVF* vvf) noexcept + : g_(g), current_edge_(e), vvf_(vvf) {} + + [[nodiscard]] constexpr value_type operator*() const { + auto target = adj_list::target(*g_, current_edge_); + return value_type{target, std::invoke(*vvf_, target)}; + } + + constexpr iterator& operator++() noexcept { + ++current_edge_; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_edge_; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_edge_ == other.current_edge_; + } + + private: + G* g_ = nullptr; + edge_type current_edge_{}; + VVF* vvf_ = nullptr; + }; + + using const_iterator = iterator; + + constexpr neighbors_view() noexcept = default; + + constexpr neighbors_view(G& g, vertex_type u, VVF vvf) noexcept(std::is_nothrow_move_constructible_v) + : g_(&g), source_(u), vvf_(std::move(vvf)) {} + + [[nodiscard]] constexpr iterator begin() noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(g_, *std::ranges::begin(edge_range), &vvf_); + } + + [[nodiscard]] constexpr iterator end() noexcept { + auto edge_range = adj_list::edges(*g_, source_); + return iterator(g_, *std::ranges::end(edge_range), &vvf_); + } + + [[nodiscard]] constexpr auto size() const noexcept + requires std::ranges::sized_range + { + return std::ranges::size(adj_list::edges(*g_, source_)); + } + +private: + G* g_ = nullptr; + vertex_type source_{}; + [[no_unique_address]] VVF vvf_{}; +}; + +// Deduction guides +template +neighbors_view(G&, adj_list::vertex_t) -> neighbors_view; + +template +neighbors_view(G&, adj_list::vertex_t, VVF) -> neighbors_view; + +/** + * @brief Create a neighbors view without value function + * + * @param g The graph to iterate over + * @param u The source vertex descriptor + * @return neighbors_view yielding neighbor_info + */ +template +[[nodiscard]] constexpr auto neighbors(G& g, adj_list::vertex_t u) noexcept { + return neighbors_view(g, u); +} + +/** + * @brief Create a neighbors view without value function (vertex_id overload) + * + * @param g The graph to iterate over + * @param uid The source vertex id + * @return neighbors_view yielding neighbor_info + */ +template +[[nodiscard]] constexpr auto neighbors(G& g, adj_list::vertex_id_t uid) noexcept { + auto u = adj_list::find_vertex(g, uid); + return neighbors_view(g, *u); +} + +/** + * @brief Create a neighbors view with value function + * + * @param g The graph to iterate over + * @param u The source vertex descriptor + * @param vvf Value function invoked for each target vertex + * @return neighbors_view yielding neighbor_info + */ +template + requires vertex_value_function> +[[nodiscard]] constexpr auto neighbors(G& g, adj_list::vertex_t u, VVF&& vvf) { + return neighbors_view>(g, u, std::forward(vvf)); +} + +/** + * @brief Create a neighbors view with value function (vertex_id overload) + * + * @param g The graph to iterate over + * @param uid The source vertex id + * @param vvf Value function invoked for each target vertex + * @return neighbors_view yielding neighbor_info + */ +template + requires vertex_value_function> +[[nodiscard]] constexpr auto neighbors(G& g, adj_list::vertex_id_t uid, VVF&& vvf) { + auto u = adj_list::find_vertex(g, uid); + return neighbors_view>(g, *u, std::forward(vvf)); +} + +} // namespace graph::views diff --git a/include/graph/views/search_base.hpp b/include/graph/views/search_base.hpp new file mode 100644 index 0000000..145cff1 --- /dev/null +++ b/include/graph/views/search_base.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +namespace graph::views { + +/// Search cancellation control for traversal algorithms +enum class cancel_search { + continue_search, ///< Continue normal traversal + cancel_branch, ///< Skip current subtree/branch, continue with siblings + cancel_all ///< Stop entire search immediately +}; + +/// Visited tracking for search views (DFS/BFS/topological sort) +/// Uses vector for space efficiency with large graphs +template > +class visited_tracker { + std::vector visited_; + +public: + /// Construct tracker for a graph with num_vertices vertices + explicit visited_tracker(std::size_t num_vertices, Alloc alloc = {}) + : visited_(num_vertices, false, alloc) {} + + /// Check if a vertex has been visited + [[nodiscard]] bool is_visited(VId id) const { + return visited_[static_cast(id)]; + } + + /// Mark a vertex as visited + void mark_visited(VId id) { visited_[static_cast(id)] = true; } + + /// Reset all vertices to unvisited state + void reset() { std::fill(visited_.begin(), visited_.end(), false); } + + /// Get the number of vertices being tracked + [[nodiscard]] std::size_t size() const { return visited_.size(); } +}; + +} // namespace graph::views diff --git a/include/graph/views/topological_sort.hpp b/include/graph/views/topological_sort.hpp new file mode 100644 index 0000000..9a517d9 --- /dev/null +++ b/include/graph/views/topological_sort.hpp @@ -0,0 +1,1006 @@ +/** + * @file topological_sort.hpp + * @brief Topological sort view for directed acyclic graphs (DAGs) + * + * Provides a view that traverses a DAG in topological order, where each vertex + * appears before all vertices it has edges to. Uses reverse DFS post-order. + * + * @complexity Time: O(V + E) where V is number of vertices and E is number of edges + * DFS visits each vertex once and traverses each edge once + * @complexity Space: O(V) for post-order vector and visited tracker + * + * @par Examples: + * @code + * // Topological sort of all vertices + * for (auto [v] : vertices_topological_sort(g)) + * process_vertex_in_topo_order(v); + * + * // With value function + * for (auto [v, val] : vertices_topological_sort(g, value_fn)) + * process_vertex_with_value(v, val); + * @endcode + * + * @note This view processes ALL vertices in the graph, not just from a single seed. + * For connected components, vertices are processed in topological order within + * each component, with components visited in arbitrary order. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declarations +template > +class vertices_topological_sort_view; + +template > +class edges_topological_sort_view; + +namespace topo_detail { + +/** + * @brief Shared topological sort state + * + * Performs DFS from all unvisited vertices, collecting post-order into a vector, + * then provides reverse iteration over that vector for topological order. + * + * @complexity Time: O(V + E) - visits each vertex once, traverses each edge once + * @complexity Space: O(V) - stores all vertices in post-order vector plus visited tracker + * When cycle detection enabled: O(2V) for additional recursion stack tracker + */ +template +struct topo_state { + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using allocator_type = Alloc; + using vertex_alloc = typename std::allocator_traits::template rebind_alloc; + + std::vector post_order_; // DFS post-order + visited_tracker visited_; + std::optional cycle_vertex_; // Set if cycle detected + std::vector rec_stack_; // Only allocated if detect_cycles=true + + topo_state(G& g, Alloc alloc = {}, bool detect_cycles = false) + : post_order_(vertex_alloc(alloc)) + , visited_(adj_list::num_vertices(g), alloc) + , rec_stack_(detect_cycles ? adj_list::num_vertices(g) : 0, false, alloc) + { + post_order_.reserve(adj_list::num_vertices(g)); + + // Perform DFS from all unvisited vertices + for (auto v : adj_list::vertices(g)) { + auto vid = adj_list::vertex_id(g, v); + if (!visited_.is_visited(vid)) { + dfs_visit(g, v, detect_cycles); + if (cycle_vertex_) { + return; // Early exit on cycle detection + } + } + } + + // Reverse for topological order (only if no cycle) + if (!cycle_vertex_) { + std::ranges::reverse(post_order_); + } + } + + [[nodiscard]] bool has_cycle() const noexcept { return cycle_vertex_.has_value(); } + [[nodiscard]] const std::optional& cycle_vertex() const noexcept { return cycle_vertex_; } + +private: + // Recursive DFS visit for topological sort + void dfs_visit(G& g, vertex_type v, bool detect_cycles) { + auto vid = adj_list::vertex_id(g, v); + visited_.mark_visited(vid); + + if (detect_cycles) { + rec_stack_[vid] = true; + } + + // Visit all children + for (auto edge : adj_list::edges(g, v)) { + auto target_v = adj_list::target(g, edge); + auto target_vid = adj_list::vertex_id(g, target_v); + + if (detect_cycles && rec_stack_[target_vid]) { + // Back edge detected - target_v closes the cycle + cycle_vertex_ = target_v; + return; + } + + if (!visited_.is_visited(target_vid)) { + dfs_visit(g, target_v, detect_cycles); + if (cycle_vertex_) { + return; // Propagate early exit + } + } + } + + if (detect_cycles) { + rec_stack_[vid] = false; + } + + // Add to post-order after all children visited + post_order_.push_back(v); + } +}; + +} // namespace topo_detail + +/** + * @brief Topological sort vertex view without value function + * + * Iterates over all vertices in topological order, yielding + * vertex_info, void> + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam Alloc Allocator type for internal containers + */ +template +class vertices_topological_sort_view + : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using allocator_type = Alloc; + using info_type = vertex_info; + +private: + using state_type = topo_detail::topo_state; + +public: + /** + * @brief Forward iterator for topological order traversal + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(std::shared_ptr state, std::size_t index) + : state_(std::move(state)), index_(index) {} + + [[nodiscard]] value_type operator*() const { + return value_type{state_->post_order_[index_]}; + } + + iterator& operator++() { + ++index_; + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + ++index_; + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + // Both at end, or both at same position in same state + if (!state_ && !other.state_) return true; + if (!state_ || !other.state_) return false; + return state_ == other.state_ && index_ == other.index_; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || index_ >= state_->post_order_.size(); + } + + private: + std::shared_ptr state_; + std::size_t index_ = 0; + }; + + /// Sentinel for end of traversal + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr vertices_topological_sort_view() noexcept = default; + + /// Construct topological sort view for entire graph + vertices_topological_sort_view(G& g, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, alloc)) + {} + + /// Construct with pre-built state (used by _safe functions) + vertices_topological_sort_view(G& g, std::shared_ptr state) + : g_(&g) + , state_(std::move(state)) + {} + + [[nodiscard]] iterator begin() { return iterator(state_, 0); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + /// Get total number of vertices in topological order + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->post_order_.size() : 0; + } + +private: + G* g_ = nullptr; + std::shared_ptr state_; +}; + +/** + * @brief Topological sort vertex view with value function + * + * Iterates over all vertices in topological order, yielding + * vertex_info, VV> where VV is the invoke result of VVF. + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam VVF Vertex value function type + * @tparam Alloc Allocator type for internal containers + */ +template +class vertices_topological_sort_view + : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using allocator_type = Alloc; + using value_result_type = std::invoke_result_t; + using info_type = vertex_info; + +private: + using state_type = topo_detail::topo_state; + +public: + /** + * @brief Forward iterator with value function + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(std::shared_ptr state, std::size_t index, VVF* vvf) + : state_(std::move(state)), index_(index), vvf_(vvf) {} + + [[nodiscard]] value_type operator*() const { + auto v = state_->post_order_[index_]; + return value_type{v, std::invoke(*vvf_, v)}; + } + + iterator& operator++() { + ++index_; + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + ++index_; + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + if (!state_ && !other.state_) return true; + if (!state_ || !other.state_) return false; + return state_ == other.state_ && index_ == other.index_; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || index_ >= state_->post_order_.size(); + } + + private: + std::shared_ptr state_; + std::size_t index_ = 0; + VVF* vvf_ = nullptr; + }; + + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr vertices_topological_sort_view() noexcept = default; + + /// Construct with value function + vertices_topological_sort_view(G& g, VVF vvf, Alloc alloc = {}) + : g_(&g) + , vvf_(std::move(vvf)) + , state_(std::make_shared(g, alloc)) + {} + + /// Construct with value function and pre-built state (used by _safe functions) + vertices_topological_sort_view(G& g, VVF vvf, std::shared_ptr state) + : g_(&g) + , vvf_(std::move(vvf)) + , state_(std::move(state)) + {} + + [[nodiscard]] iterator begin() { return iterator(state_, 0, &vvf_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + + [[nodiscard]] std::size_t size() const noexcept { + return state_ ? state_->post_order_.size() : 0; + } + +private: + G* g_ = nullptr; + [[no_unique_address]] VVF vvf_{}; + std::shared_ptr state_; +}; + +// Deduction guides +template +vertices_topological_sort_view(G&, Alloc) -> vertices_topological_sort_view; + +template +vertices_topological_sort_view(G&) -> vertices_topological_sort_view>; + +template +vertices_topological_sort_view(G&, VVF, Alloc) -> vertices_topological_sort_view; + +/** + * @brief Topological sort edge view without value function + * + * Iterates over all edges in topological order (by source vertex), yielding + * edge_info, void> + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam Alloc Allocator type for internal containers + */ +template +class edges_topological_sort_view + : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using info_type = edge_info; + +private: + using state_type = topo_detail::topo_state; + +public: + /** + * @brief Forward iterator for edges in topological order + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state, std::size_t v_idx) + : g_(g), state_(std::move(state)), vertex_index_(v_idx) { + if (!at_end()) { + auto v = state_->post_order_[vertex_index_]; + auto edge_range = adj_list::edges(*g_, v); + edge_it_ = std::ranges::begin(edge_range); + edge_end_ = std::ranges::end(edge_range); + advance_to_next_edge(); + } + } + + [[nodiscard]] value_type operator*() const { + return value_type{*edge_it_}; + } + + iterator& operator++() { + ++edge_it_; + advance_to_next_edge(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + ++edge_it_; + advance_to_next_edge(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + if (at_end() && other.at_end()) return true; + if (at_end() || other.at_end()) return false; + return vertex_index_ == other.vertex_index_ && edge_it_ == other.edge_it_; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || vertex_index_ >= state_->post_order_.size(); + } + + private: + void advance_to_next_edge() { + // Find next edge, advancing to next vertex if needed + while (vertex_index_ < state_->post_order_.size()) { + if (edge_it_ != edge_end_) { + return; // Found an edge + } + + // Move to next vertex + ++vertex_index_; + if (vertex_index_ < state_->post_order_.size()) { + auto v = state_->post_order_[vertex_index_]; + auto edge_range = adj_list::edges(*g_, v); + edge_it_ = std::ranges::begin(edge_range); + edge_end_ = std::ranges::end(edge_range); + } + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + std::size_t vertex_index_ = 0; + adj_list::vertex_edge_iterator_t edge_it_{}; + adj_list::vertex_edge_iterator_t edge_end_{}; + }; + + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr edges_topological_sort_view() noexcept = default; + + edges_topological_sort_view(G& g, Alloc alloc = {}) + : g_(&g) + , state_(std::make_shared(g, alloc)) + {} + + /// Construct with pre-built state (used by _safe functions) + edges_topological_sort_view(G& g, std::shared_ptr state) + : g_(&g) + , state_(std::move(state)) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_, 0); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + +private: + G* g_ = nullptr; + std::shared_ptr state_; +}; + +/** + * @brief Topological sort edge view with value function + * + * Iterates over all edges in topological order (by source vertex), yielding + * edge_info, EV> where EV is the invoke result of EVF. + * + * @tparam G Graph type satisfying index_adjacency_list concept + * @tparam EVF Edge value function type + * @tparam Alloc Allocator type for internal containers + */ +template +class edges_topological_sort_view + : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using edge_type = adj_list::edge_t; + using allocator_type = Alloc; + using value_result_type = std::invoke_result_t; + using info_type = edge_info; + +private: + using state_type = topo_detail::topo_state; + +public: + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + iterator(G* g, std::shared_ptr state, std::size_t v_idx, EVF* evf) + : g_(g), state_(std::move(state)), vertex_index_(v_idx), evf_(evf) { + if (!at_end()) { + auto v = state_->post_order_[vertex_index_]; + auto edge_range = adj_list::edges(*g_, v); + edge_it_ = std::ranges::begin(edge_range); + edge_end_ = std::ranges::end(edge_range); + advance_to_next_edge(); + } + } + + [[nodiscard]] value_type operator*() const { + return value_type{*edge_it_, std::invoke(*evf_, *edge_it_)}; + } + + iterator& operator++() { + ++edge_it_; + advance_to_next_edge(); + return *this; + } + + iterator operator++(int) { + auto tmp = *this; + ++edge_it_; + advance_to_next_edge(); + return tmp; + } + + [[nodiscard]] bool operator==(const iterator& other) const noexcept { + if (at_end() && other.at_end()) return true; + if (at_end() || other.at_end()) return false; + return vertex_index_ == other.vertex_index_ && edge_it_ == other.edge_it_; + } + + [[nodiscard]] bool at_end() const noexcept { + return !state_ || vertex_index_ >= state_->post_order_.size(); + } + + private: + void advance_to_next_edge() { + while (vertex_index_ < state_->post_order_.size()) { + if (edge_it_ != edge_end_) { + return; + } + + ++vertex_index_; + if (vertex_index_ < state_->post_order_.size()) { + auto v = state_->post_order_[vertex_index_]; + auto edge_range = adj_list::edges(*g_, v); + edge_it_ = std::ranges::begin(edge_range); + edge_end_ = std::ranges::end(edge_range); + } + } + } + + G* g_ = nullptr; + std::shared_ptr state_; + std::size_t vertex_index_ = 0; + adj_list::vertex_edge_iterator_t edge_it_{}; + adj_list::vertex_edge_iterator_t edge_end_{}; + EVF* evf_ = nullptr; + }; + + struct sentinel { + [[nodiscard]] constexpr bool operator==(const iterator& it) const noexcept { + return it.at_end(); + } + }; + + constexpr edges_topological_sort_view() noexcept = default; + + edges_topological_sort_view(G& g, EVF evf, Alloc alloc = {}) + : g_(&g) + , evf_(std::move(evf)) + , state_(std::make_shared(g, alloc)) + {} + + /// Construct with value function and pre-built state (used by _safe functions) + edges_topological_sort_view(G& g, EVF evf, std::shared_ptr state) + : g_(&g) + , evf_(std::move(evf)) + , state_(std::move(state)) + {} + + [[nodiscard]] iterator begin() { return iterator(g_, state_, 0, &evf_); } + [[nodiscard]] sentinel end() const noexcept { return {}; } + +private: + G* g_ = nullptr; + [[no_unique_address]] EVF evf_{}; + std::shared_ptr state_; +}; + +// Deduction guides for edges +template +edges_topological_sort_view(G&, Alloc) -> edges_topological_sort_view; + +template +edges_topological_sort_view(G&) -> edges_topological_sort_view>; + +template +edges_topological_sort_view(G&, EVF, Alloc) -> edges_topological_sort_view; + +//============================================================================= +// Factory functions +//============================================================================= + +/** + * @brief Create a topological sort vertex view without value function + * + * @param g The graph to traverse + * @return vertices_topological_sort_view yielding vertices in topological order + * + * @complexity Time: O(V + E) - DFS traversal of entire graph + * @complexity Space: O(V) - stores all vertices in result vector + */ +template +[[nodiscard]] auto vertices_topological_sort(G& g) { + return vertices_topological_sort_view>(g, std::allocator{}); +} + +/** + * @brief Create a topological sort vertex view with value function + * + * @param g The graph to traverse + * @param vvf Value function invoked for each vertex + * @return vertices_topological_sort_view with values + * + * @complexity Time: O(V + E) - DFS traversal plus value function invocations + * @complexity Space: O(V) - stores all vertices in result vector + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_topological_sort(G& g, VVF&& vvf) { + return vertices_topological_sort_view, std::allocator>( + g, std::forward(vvf), std::allocator{}); +} + +/** + * @brief Create a topological sort vertex view with custom allocator + * + * @param g The graph to traverse + * @param alloc Allocator for internal containers + * @return vertices_topological_sort_view with custom allocator + * + * @complexity Time: O(V + E) - DFS traversal of entire graph + * @complexity Space: O(V) - stores all vertices in result vector + */ +template + requires (!vertex_value_function>) +[[nodiscard]] auto vertices_topological_sort(G& g, Alloc alloc) { + return vertices_topological_sort_view(g, alloc); +} + +/** + * @brief Create a topological sort vertex view with value function and custom allocator + * + * @param g The graph to traverse + * @param vvf Value function invoked for each vertex + * @param alloc Allocator for internal containers + * @return vertices_topological_sort_view with value function and custom allocator + * + * @complexity Time: O(V + E) - DFS traversal plus value function invocations + * @complexity Space: O(V) - stores all vertices in result vector + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_topological_sort(G& g, VVF&& vvf, Alloc alloc) { + return vertices_topological_sort_view, Alloc>(g, std::forward(vvf), alloc); +} + +/** + * @brief Create a topological sort edge view without value function + * + * @param g The graph to traverse + * @return edges_topological_sort_view yielding edges in topological order (by source vertex) + * + * @complexity Time: O(V + E) - DFS traversal plus edge iteration + * @complexity Space: O(V) - stores all vertices in result vector + */ +template +[[nodiscard]] auto edges_topological_sort(G& g) { + return edges_topological_sort_view>(g, std::allocator{}); +} + +/** + * @brief Create a topological sort edge view with value function + * + * @param g The graph to traverse + * @param evf Value function invoked for each edge + * @return edges_topological_sort_view with values + * + * @complexity Time: O(V + E) - DFS traversal plus value function invocations + * @complexity Space: O(V) - stores all vertices in result vector + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_topological_sort(G& g, EVF&& evf) { + return edges_topological_sort_view, std::allocator>( + g, std::forward(evf), std::allocator{}); +} + +/** + * @brief Create a topological sort edge view with custom allocator + * + * @param g The graph to traverse + * @param alloc Allocator for internal containers + * @return edges_topological_sort_view with custom allocator + * + * @complexity Time: O(V + E) - DFS traversal plus edge iteration + * @complexity Space: O(V) - stores all vertices in result vector + */ +template + requires (!edge_value_function>) +[[nodiscard]] auto edges_topological_sort(G& g, Alloc alloc) { + return edges_topological_sort_view(g, alloc); +} + +/** + * @brief Create a topological sort edge view with value function and custom allocator + * + * @param g The graph to traverse + * @param evf Value function invoked for each edge + * @param alloc Allocator for internal containers + * @return edges_topological_sort_view with value function and custom allocator + * + * @complexity Time: O(V + E) - DFS traversal plus value function invocations + * @complexity Space: O(V) - stores all vertices in result vector + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_topological_sort(G& g, EVF&& evf, Alloc alloc) { + return edges_topological_sort_view, Alloc>(g, std::forward(evf), alloc); +} + +//============================================================================= +// Safe factory functions with cycle detection +//============================================================================= + +/** + * @brief Create a topological sort vertex view with cycle detection + * + * Returns a view on success, or the vertex that closes a cycle if one is detected. + * Uses DFS with recursion stack tracking to detect back edges (cycles). + * + * @param g The graph to traverse + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection + * Space: O(2V) - post-order vector + visited tracker + recursion stack + * + * @par Example: + * @code + * auto result = vertices_topological_sort_safe(g); + * if (result) { + * for (auto [v] : *result) { + * process_vertex(v); + * } + * } else { + * auto cycle_v = result.error(); + * std::cerr << "Cycle detected at vertex " << vertex_id(g, cycle_v) << "\n"; + * } + * @endcode + */ +template +[[nodiscard]] auto vertices_topological_sort_safe(G& g) + -> tl::expected>, + adj_list::vertex_t> +{ + using view_type = vertices_topological_sort_view>; + + auto state = std::make_shared>>( + g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::move(state)); +} + +/** + * @brief Create a topological sort vertex view with value function and cycle detection + * + * @param g The graph to traverse + * @param vvf Value function invoked for each vertex + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection plus value function invocations + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_topological_sort_safe(G& g, VVF&& vvf) + -> tl::expected, std::allocator>, + adj_list::vertex_t> +{ + using view_type = vertices_topological_sort_view, std::allocator>; + + auto state = std::make_shared>>( + g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::forward(vvf), std::move(state)); +} + +/** + * @brief Create a topological sort vertex view with custom allocator and cycle detection + * + * @param g The graph to traverse + * @param alloc Allocator for internal containers + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template + requires (!vertex_value_function>) +[[nodiscard]] auto vertices_topological_sort_safe(G& g, Alloc alloc) + -> tl::expected, + adj_list::vertex_t> +{ + using view_type = vertices_topological_sort_view; + + auto state = std::make_shared>(g, alloc, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::move(state)); +} + +/** + * @brief Create a topological sort vertex view with value function, allocator, and cycle detection + * + * @param g The graph to traverse + * @param vvf Value function invoked for each vertex + * @param alloc Allocator for internal containers + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection plus value function invocations + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template + requires vertex_value_function> +[[nodiscard]] auto vertices_topological_sort_safe(G& g, VVF&& vvf, Alloc alloc) + -> tl::expected, Alloc>, + adj_list::vertex_t> +{ + using view_type = vertices_topological_sort_view, Alloc>; + + auto state = std::make_shared>(g, alloc, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::forward(vvf), std::move(state)); +} + +/** + * @brief Create a topological sort edge view with cycle detection + * + * Returns a view on success, or the vertex that closes a cycle if one is detected. + * + * @param g The graph to traverse + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection plus edge iteration + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template +[[nodiscard]] auto edges_topological_sort_safe(G& g) + -> tl::expected>, + adj_list::vertex_t> +{ + using view_type = edges_topological_sort_view>; + + auto state = std::make_shared>>( + g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::move(state)); +} + +/** + * @brief Create a topological sort edge view with value function and cycle detection + * + * @param g The graph to traverse + * @param evf Value function invoked for each edge + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection plus value function invocations + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_topological_sort_safe(G& g, EVF&& evf) + -> tl::expected, std::allocator>, + adj_list::vertex_t> +{ + using view_type = edges_topological_sort_view, std::allocator>; + + auto state = std::make_shared>>( + g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::forward(evf), std::move(state)); +} + +/** + * @brief Create a topological sort edge view with custom allocator and cycle detection + * + * @param g The graph to traverse + * @param alloc Allocator for internal containers + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection plus edge iteration + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template + requires (!edge_value_function>) +[[nodiscard]] auto edges_topological_sort_safe(G& g, Alloc alloc) + -> tl::expected, + adj_list::vertex_t> +{ + using view_type = edges_topological_sort_view; + + auto state = std::make_shared>(g, alloc, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::move(state)); +} + +/** + * @brief Create a topological sort edge view with value function, allocator, and cycle detection + * + * @param g The graph to traverse + * @param evf Value function invoked for each edge + * @param alloc Allocator for internal containers + * @return expected containing view on success, or cycle vertex on failure + * + * @complexity Time: O(V + E) - DFS traversal with cycle detection plus value function invocations + * Space: O(2V) - post-order vector + visited tracker + recursion stack + */ +template + requires edge_value_function> +[[nodiscard]] auto edges_topological_sort_safe(G& g, EVF&& evf, Alloc alloc) + -> tl::expected, Alloc>, + adj_list::vertex_t> +{ + using view_type = edges_topological_sort_view, Alloc>; + + auto state = std::make_shared>(g, alloc, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::forward(evf), std::move(state)); +} + +} // namespace graph::views diff --git a/include/graph/views/vertexlist.hpp b/include/graph/views/vertexlist.hpp new file mode 100644 index 0000000..d4bf471 --- /dev/null +++ b/include/graph/views/vertexlist.hpp @@ -0,0 +1,280 @@ +/** + * @file vertexlist.hpp + * @brief Vertexlist view for iterating over graph vertices + * + * Provides a view that iterates over all vertices in a graph, yielding + * vertex_info for each vertex. Supports + * optional value functions to compute per-vertex values. + * + * @section chaining_with_std_views Chaining with std::views + * + * Views created WITHOUT value functions chain perfectly with std::views: + * @code + * auto view = g | vertexlist() + * | std::views::transform([&g](auto vi) { return vertex_id(g, vi.vertex); }) + * | std::views::filter([](auto id) { return id > 0; }); + * @endcode + * + * @warning LIMITATION: Views with capturing lambda value functions cannot chain with std::views + * + * This FAILS to compile in C++20: + * @code + * auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; + * auto view = g | vertexlist(vvf) | std::views::take(2); // ❌ Won't compile + * @endcode + * + * The reason: When a view stores a capturing lambda as VVF, the lambda is not + * default_initializable or semiregular, which prevents the view from satisfying + * std::ranges::view. This is a fundamental C++20 limitation. + * + * @section workarounds Workarounds + * + * 1. RECOMMENDED: Use views without value functions, then transform: + * @code + * auto view = g | vertexlist() + * | std::views::transform([&g](auto vi) { + * return std::make_tuple(vi.vertex, vertex_id(g, vi.vertex) * 10); + * }); + * @endcode + * + * 2. Don't chain - use value functions standalone: + * @code + * auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; + * auto view = g | vertexlist(vvf); // ✅ Works fine, just don't chain further + * @endcode + * + * 3. Extract to container first, then chain: + * @code + * std::vector> vertices; + * for (auto vi : g | vertexlist()) vertices.push_back(vi); + * auto view = vertices | std::views::transform(...); + * @endcode + * + * @section cpp26_fix C++26 Fix + * + * C++26 will introduce std::copyable_function (P2548) or similar type-erased + * function wrappers that are always semiregular, which will solve this issue. + * Future implementation could wrap VVF in such a type to enable chaining + * with capturing lambdas. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graph::views { + +// Forward declaration +template +class vertexlist_view; + +/** + * @brief Vertexlist view without value function + * + * Iterates over vertices yielding vertex_info, void> + * + * @tparam G Graph type satisfying adjacency_list concept + */ +template +class vertexlist_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using info_type = vertex_info; + + /** + * @brief Forward iterator yielding vertex_info values + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = const value_type&; + + constexpr iterator() noexcept = default; + + constexpr iterator(G* g, vertex_type v) noexcept + : g_(g), current_{v} {} + + [[nodiscard]] constexpr reference operator*() const noexcept { + return current_; + } + + constexpr iterator& operator++() noexcept { + ++current_.vertex; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_.vertex; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_.vertex == other.current_.vertex; + } + + private: + G* g_ = nullptr; + value_type current_{}; + }; + + using const_iterator = iterator; + + constexpr vertexlist_view() noexcept = default; + + constexpr explicit vertexlist_view(G& g) noexcept + : g_(&g) {} + + [[nodiscard]] constexpr iterator begin() const noexcept { + auto vert_range = adj_list::vertices(*g_); + return iterator(g_, *std::ranges::begin(vert_range)); + } + + [[nodiscard]] constexpr iterator end() const noexcept { + auto vert_range = adj_list::vertices(*g_); + return iterator(g_, *std::ranges::end(vert_range)); + } + + [[nodiscard]] constexpr std::size_t size() const noexcept { + return adj_list::num_vertices(*g_); + } + +private: + G* g_ = nullptr; +}; + +/** + * @brief Vertexlist view with value function + * + * Iterates over vertices yielding vertex_info, VV> + * where VV is the result of invoking the value function on the vertex descriptor. + * + * @tparam G Graph type satisfying adjacency_list concept + * @tparam VVF Value function type + */ +template +class vertexlist_view : public std::ranges::view_interface> { +public: + using graph_type = G; + using vertex_type = adj_list::vertex_t; + using vertex_id_type = adj_list::vertex_id_t; + using value_type_result = std::invoke_result_t; + using info_type = vertex_info; + + /** + * @brief Forward iterator yielding vertex_info values with computed value + */ + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = info_type; + using pointer = const value_type*; + using reference = value_type; + + constexpr iterator() noexcept = default; + + constexpr iterator(G* g, vertex_type v, VVF* vvf) noexcept + : g_(g), current_(v), vvf_(vvf) {} + + [[nodiscard]] constexpr value_type operator*() const { + return value_type{current_, std::invoke(*vvf_, current_)}; + } + + constexpr iterator& operator++() noexcept { + ++current_; + return *this; + } + + constexpr iterator operator++(int) noexcept { + auto tmp = *this; + ++current_; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(const iterator& other) const noexcept { + return current_ == other.current_; + } + + private: + G* g_ = nullptr; + vertex_type current_{}; + VVF* vvf_ = nullptr; + }; + + using const_iterator = iterator; + + constexpr vertexlist_view() noexcept = default; + + constexpr vertexlist_view(vertexlist_view&&) = default; + constexpr vertexlist_view& operator=(vertexlist_view&&) = default; + + constexpr vertexlist_view(const vertexlist_view&) = default; + constexpr vertexlist_view& operator=(const vertexlist_view&) = default; + + constexpr vertexlist_view(G& g, VVF vvf) noexcept(std::is_nothrow_move_constructible_v) + : g_(&g), vvf_(std::move(vvf)) {} + + [[nodiscard]] constexpr iterator begin() noexcept { + auto vert_range = adj_list::vertices(*g_); + return iterator(g_, *std::ranges::begin(vert_range), &vvf_); + } + + [[nodiscard]] constexpr iterator end() noexcept { + auto vert_range = adj_list::vertices(*g_); + return iterator(g_, *std::ranges::end(vert_range), &vvf_); + } + + [[nodiscard]] constexpr std::size_t size() const noexcept { + return adj_list::num_vertices(*g_); + } + +private: + G* g_ = nullptr; + [[no_unique_address]] VVF vvf_{}; +}; + +// Deduction guides +template +vertexlist_view(G&) -> vertexlist_view; + +template +vertexlist_view(G&, VVF) -> vertexlist_view; + +/** + * @brief Create a vertexlist view without value function + * + * @param g The graph to iterate over + * @return vertexlist_view yielding vertex_info + */ +template +[[nodiscard]] constexpr auto vertexlist(G& g) noexcept { + return vertexlist_view(g); +} + +/** + * @brief Create a vertexlist view with value function + * + * @param g The graph to iterate over + * @param vvf Value function invoked for each vertex + * @return vertexlist_view yielding vertex_info + */ +template + requires vertex_value_function> +[[nodiscard]] constexpr auto vertexlist(G& g, VVF&& vvf) { + return vertexlist_view>(g, std::forward(vvf)); +} + +} // namespace graph::views diff --git a/include/graph/views/view_concepts.hpp b/include/graph/views/view_concepts.hpp new file mode 100644 index 0000000..ede77a2 --- /dev/null +++ b/include/graph/views/view_concepts.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace graph::views { + +/// Concept for types that can be used as vertex value functions +/// Value functions must be invocable with a vertex descriptor and return a non-void type +template +concept vertex_value_function = + std::invocable && + (!std::is_void_v>); + +/// Concept for types that can be used as edge value functions +/// Value functions must be invocable with an edge descriptor and return a non-void type +template +concept edge_value_function = + std::invocable && + (!std::is_void_v>); + +/// Concept for search views (DFS/BFS/topological sort) +/// Search views provide depth(), size() accessors and cancel() control +template +concept search_view = requires(V& v, const V& cv) { + { v.cancel() } -> std::convertible_to; + { cv.depth() } -> std::convertible_to; + { cv.size() } -> std::convertible_to; +}; + +} // namespace graph::views diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2d8b01f..2781f0c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,8 @@ include(Catch) # Add subdirectories with their own test executables add_subdirectory(adj_list) add_subdirectory(container) +add_subdirectory(edge_list) +add_subdirectory(views) # Keep test_main.cpp in root for now (can be moved to common/ later) # No main executable needed - each subdirectory creates its own diff --git a/tests/adj_list/concepts/test_adjacency_list_vertex_concepts.cpp b/tests/adj_list/concepts/test_adjacency_list_vertex_concepts.cpp index b2a9ba8..4a95a48 100644 --- a/tests/adj_list/concepts/test_adjacency_list_vertex_concepts.cpp +++ b/tests/adj_list/concepts/test_adjacency_list_vertex_concepts.cpp @@ -89,12 +89,14 @@ TEST_CASE("vertex_range concept - empty graph", "[adjacency_list][concepts][vert TEST_CASE("index_vertex_range concept - vector>", "[adjacency_list][concepts][index_vertex_range]") { using Graph = std::vector>; - // Vector's underlying storage is random access, but vertex_descriptor_view - // only provides forward iteration (descriptors are synthesized on-the-fly) - STATIC_REQUIRE_FALSE(index_vertex_range); + // Vector's underlying iterator is random access, so index_vertex_range is satisfied + // Note: vertex_descriptor_view itself is forward-only, but we check the underlying iterator + STATIC_REQUIRE(index_vertex_range); + + // The view is still forward-only (descriptors synthesized on-the-fly) STATIC_REQUIRE_FALSE(std::ranges::random_access_range>); - // But it does satisfy vertex_range + // And it satisfies vertex_range STATIC_REQUIRE(vertex_range); Graph g = {{1, 2}, {2, 3}, {0}}; @@ -112,11 +114,13 @@ TEST_CASE("index_vertex_range concept - vector>", "[adjacency_list][ TEST_CASE("index_vertex_range concept - deque>", "[adjacency_list][concepts][index_vertex_range]") { using Graph = std::deque>; - // Deque's iterator is random access, but vertex_descriptor_view only provides forward iteration - STATIC_REQUIRE_FALSE(index_vertex_range); + // Deque's underlying iterator is random access, so index_vertex_range is satisfied + STATIC_REQUIRE(index_vertex_range); + + // The view is still forward-only STATIC_REQUIRE_FALSE(std::ranges::random_access_range>); - // But it does satisfy the basic vertex_range + // And it satisfies the basic vertex_range STATIC_REQUIRE(vertex_range); } @@ -204,12 +208,10 @@ TEST_CASE("adjacency_list concept - empty graph", "[adjacency_list][concepts][gr TEST_CASE("index_adjacency_list concept - vector>", "[adjacency_list][concepts][index_graph]") { using Graph = std::vector>; - // Currently, vertex_descriptor_view only provides forward iteration - // So vector-based graphs don't satisfy index_adjacency_list - STATIC_REQUIRE_FALSE(index_adjacency_list); + // Vector's underlying iterator is random access, so index_adjacency_list is satisfied + STATIC_REQUIRE(index_adjacency_list); - // But they do satisfy adjacency_list - STATIC_REQUIRE(adjacency_list); + // And they satisfy adjacency_list STATIC_REQUIRE(adjacency_list); Graph g = {{1, 2}, {2, 3}, {0}}; @@ -227,8 +229,8 @@ TEST_CASE("index_adjacency_list concept - vector>", "[adjacency_list TEST_CASE("index_adjacency_list concept - deque>", "[adjacency_list][concepts][index_graph]") { using Graph = std::deque>; - // Deque doesn't satisfy index_adjacency_list because vertex_descriptor_view is forward-only - STATIC_REQUIRE_FALSE(index_adjacency_list); + // Deque's underlying iterator is random access, so index_adjacency_list is satisfied + STATIC_REQUIRE(index_adjacency_list); STATIC_REQUIRE(adjacency_list); } @@ -248,15 +250,13 @@ TEST_CASE("index_adjacency_list concept - map does NOT satisfy", "[adjacency_lis TEST_CASE("Concept hierarchy - index_adjacency_list implies adjacency_list", "[adjacency_list][concepts][hierarchy]") { using Graph1 = std::vector>; - // Currently vertex_descriptor_view is forward-only, so index_adjacency_list is not satisfied - // even for vector-based graphs - STATIC_REQUIRE_FALSE(index_adjacency_list); + // Vector's underlying iterator is random access, so index_adjacency_list is satisfied + STATIC_REQUIRE(index_adjacency_list); STATIC_REQUIRE(adjacency_list); - // Deque satisfies adjacency_list but not index_adjacency_list - // because vertex_descriptor_view only provides forward iteration + // Deque also satisfies index_adjacency_list (random access underlying iterator) using Graph2 = std::deque>; - STATIC_REQUIRE_FALSE(index_adjacency_list); + STATIC_REQUIRE(index_adjacency_list); STATIC_REQUIRE(adjacency_list); } @@ -265,12 +265,11 @@ TEST_CASE("Concept hierarchy - index_vertex_range implies vertex_range", "[adjac using Graph2 = std::map>; - // Currently, vertex_descriptor_view only provides forward iteration - // So even vector-based graphs don't satisfy index_vertex_range - STATIC_REQUIRE_FALSE(index_vertex_range); + // Vector's underlying iterator is random access, so index_vertex_range is satisfied + STATIC_REQUIRE(index_vertex_range); STATIC_REQUIRE(vertex_range); - // But not all vertex_ranges are index_vertex_ranges + // Map's underlying iterator is NOT random access, so index_vertex_range is NOT satisfied STATIC_REQUIRE(vertex_range); STATIC_REQUIRE_FALSE(index_vertex_range); } @@ -283,8 +282,8 @@ TEST_CASE("Concepts work with actual graph operations", "[adjacency_list][concep using Graph = std::vector>; STATIC_REQUIRE(adjacency_list); - // Note: index_adjacency_list not satisfied because vertex_descriptor_view is forward-only - STATIC_REQUIRE_FALSE(index_adjacency_list); + // Vector's underlying iterator is random access, so index_adjacency_list is satisfied + STATIC_REQUIRE(index_adjacency_list); Graph g = {{1, 2, 3}, {0, 2, 3}, {0, 1, 3}, {0, 1, 2}}; @@ -307,11 +306,11 @@ TEST_CASE("Concepts work with actual graph operations", "[adjacency_list][concep } TEST_CASE("Concepts distinguish container types correctly", "[adjacency_list][concepts][integration]") { - // All container types satisfy adjacency_list - // None satisfy index_adjacency_list because vertex_descriptor_view is forward-only + // Vector and deque satisfy index_adjacency_list (random access underlying iterator) + // Map does NOT (bidirectional iterator) using VectorGraph = std::vector>; - STATIC_REQUIRE_FALSE(index_adjacency_list); + STATIC_REQUIRE(index_adjacency_list); STATIC_REQUIRE(adjacency_list); using MapGraph = std::map>; @@ -319,6 +318,6 @@ TEST_CASE("Concepts distinguish container types correctly", "[adjacency_list][co STATIC_REQUIRE_FALSE(index_adjacency_list); using DequeGraph = std::deque>; - STATIC_REQUIRE_FALSE(index_adjacency_list); + STATIC_REQUIRE(index_adjacency_list); STATIC_REQUIRE(adjacency_list); } diff --git a/tests/edge_list/CMakeLists.txt b/tests/edge_list/CMakeLists.txt new file mode 100644 index 0000000..e740a8e --- /dev/null +++ b/tests/edge_list/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +# Consolidated edge_list test executable +add_executable(test_edge_list + test_edge_list_cpo.cpp + test_edge_list_descriptor.cpp + test_edge_list_concepts.cpp + test_edge_list_integration.cpp +) +target_link_libraries(test_edge_list PRIVATE graph3 Catch2::Catch2WithMain) +add_test(NAME test_edge_list COMMAND test_edge_list) diff --git a/tests/edge_list/test_edge_list_concepts.cpp b/tests/edge_list/test_edge_list_concepts.cpp new file mode 100644 index 0000000..3a87500 --- /dev/null +++ b/tests/edge_list/test_edge_list_concepts.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include + +using namespace graph; +// Don't use `using namespace graph::edge_list` to avoid ambiguity with adj_list::edge_descriptor + + +// ============================================================================= +// Concept Satisfaction Tests +// ============================================================================= + +TEST_CASE("basic_sourced_edgelist concept with pairs", "[edge_list][concepts]") { + using edge_list_type = std::vector>; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(!edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with 2-tuples", "[edge_list][concepts]") { + using edge_list_type = std::vector>; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(!edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with 3-tuples", "[edge_list][concepts]") { + using edge_list_type = std::vector>; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with edge_info (no value)", "[edge_list][concepts]") { + using edge_type = graph::edge_info; + using edge_list_type = std::vector; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(!edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with edge_info (with value)", "[edge_list][concepts]") { + using edge_type = graph::edge_info; + using edge_list_type = std::vector; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with edge_descriptor (no value)", "[edge_list][concepts]") { + using edge_list_type = std::vector>; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(!edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with edge_descriptor (with value)", "[edge_list][concepts]") { + using edge_list_type = std::vector>; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist); + STATIC_REQUIRE(edge_list::has_edge_value); +} + +TEST_CASE("basic_sourced_edgelist concept with string vertex IDs", "[edge_list][concepts]") { + using edge_list_type = std::vector>; + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + STATIC_REQUIRE(!edge_list::basic_sourced_index_edgelist); // strings are not integral + STATIC_REQUIRE(!edge_list::has_edge_value); +} + +TEST_CASE("Nested ranges should NOT satisfy basic_sourced_edgelist", "[edge_list][concepts]") { + // Adjacency list pattern - vector of vectors + using nested_type = std::vector>; + STATIC_REQUIRE(!edge_list::basic_sourced_edgelist); +} + +// ============================================================================= +// Type Alias Tests +// ============================================================================= + +TEST_CASE("edge_list type aliases", "[edge_list][types]") { + using EL = std::vector>; + + // These should compile + using edge_range = edge_list::edge_range_t; + using edge_iter = edge_list::edge_iterator_t; + using edge = edge_list::edge_t; + using edge_ref = edge_list::edge_reference_t; + using edge_val = edge_list::edge_value_t; + using vid = edge_list::vertex_id_t; + + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + SUCCEED("Type aliases compile successfully"); +} + +TEST_CASE("edge_list type aliases without edge value", "[edge_list][types]") { + using EL = std::vector>; + + using edge_range = edge_list::edge_range_t; + using edge_iter = edge_list::edge_iterator_t; + using edge = edge_list::edge_t; + using edge_ref = edge_list::edge_reference_t; + using vid = edge_list::vertex_id_t; + + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v); + + SUCCEED("Type aliases compile successfully"); +} + +// ============================================================================= +// Runtime Behavior Tests +// ============================================================================= + +TEST_CASE("basic_sourced_edgelist runtime behavior with pairs", "[edge_list][runtime]") { + std::vector> edges = {{1, 2}, {2, 3}, {3, 4}}; + + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + + for (const auto& e : edges) { + auto src = graph::adj_list::_cpo_instances::source_id(edges, e); + auto tgt = graph::adj_list::_cpo_instances::target_id(edges, e); + REQUIRE(src < tgt); // All our test edges have src < tgt + } +} + +TEST_CASE("basic_sourced_edgelist runtime behavior with edge_descriptor", "[edge_list][runtime]") { + int s1 = 1, t1 = 2; + int s2 = 2, t2 = 3; + + edge_list::edge_descriptor e1(s1, t1); + edge_list::edge_descriptor e2(s2, t2); + + std::vector> edges = {e1, e2}; + + STATIC_REQUIRE(edge_list::basic_sourced_edgelist); + + auto src = graph::adj_list::_cpo_instances::source_id(edges, e1); + auto tgt = graph::adj_list::_cpo_instances::target_id(edges, e1); + + REQUIRE(src == 1); + REQUIRE(tgt == 2); +} + +TEST_CASE("has_edge_value runtime behavior", "[edge_list][runtime]") { + int s = 5, t = 6; + double v = 3.14; + edge_list::edge_descriptor e(s, t, v); + + std::vector> edges = {e}; + + STATIC_REQUIRE(edge_list::has_edge_value); + + auto val = graph::adj_list::_cpo_instances::edge_value(edges, e); + REQUIRE(val == 3.14); +} diff --git a/tests/edge_list/test_edge_list_cpo.cpp b/tests/edge_list/test_edge_list_cpo.cpp new file mode 100644 index 0000000..2f6282f --- /dev/null +++ b/tests/edge_list/test_edge_list_cpo.cpp @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::adj_list::_cpo_instances; + +// ============================================================================= +// Tier 6 Tests: edge_info data member access +// ============================================================================= + +TEST_CASE("source_id with edge_info (bidirectional, no value)", "[cpo][source_id][tier6]") { + using EI = edge_info; + EI ei{1, 2}; + std::vector el{ei}; + + auto uid = source_id(el, ei); + REQUIRE(uid == 1); +} + +TEST_CASE("source_id with edge_info (bidirectional, with value)", "[cpo][source_id][tier6]") { + using EI = edge_info; + EI ei{3, 4, 1.5}; + std::vector el{ei}; + + auto uid = source_id(el, ei); + REQUIRE(uid == 3); +} + +TEST_CASE("target_id with edge_info (bidirectional, no value)", "[cpo][target_id][tier6]") { + using EI = edge_info; + EI ei{5, 6}; + std::vector el{ei}; + + auto vid = target_id(el, ei); + REQUIRE(vid == 6); +} + +TEST_CASE("target_id with edge_info (bidirectional, with value)", "[cpo][target_id][tier6]") { + using EI = edge_info; + EI ei{7, 8, 2.5}; + std::vector el{ei}; + + auto vid = target_id(el, ei); + REQUIRE(vid == 8); +} + +TEST_CASE("edge_value with edge_info (with value)", "[cpo][edge_value][tier6]") { + using EI = edge_info; + EI ei{9, 10, 3.5}; + std::vector el{ei}; + + auto val = edge_value(el, ei); + REQUIRE(val == 3.5); +} + +TEST_CASE("edge_value with edge_info (unidirectional, with value)", "[cpo][edge_value][tier6]") { + using EI = edge_info; + EI ei{11, 4.5}; + std::vector el{ei}; + + auto val = edge_value(el, ei); + REQUIRE(val == 4.5); +} + +// ============================================================================= +// Tier 7 Tests: tuple-like edge access +// ============================================================================= + +TEST_CASE("source_id with pair", "[cpo][source_id][tier7]") { + std::pair edge{12, 13}; + std::vector> el{edge}; + + auto uid = source_id(el, edge); + REQUIRE(uid == 12); +} + +TEST_CASE("target_id with pair", "[cpo][target_id][tier7]") { + std::pair edge{14, 15}; + std::vector> el{edge}; + + auto vid = target_id(el, edge); + REQUIRE(vid == 15); +} + +TEST_CASE("source_id with tuple (3 elements)", "[cpo][source_id][tier7]") { + std::tuple edge{16, 17, 5.5}; + std::vector> el{edge}; + + auto uid = source_id(el, edge); + REQUIRE(uid == 16); +} + +TEST_CASE("target_id with tuple (3 elements)", "[cpo][target_id][tier7]") { + std::tuple edge{18, 19, 6.5}; + std::vector> el{edge}; + + auto vid = target_id(el, edge); + REQUIRE(vid == 19); +} + +TEST_CASE("edge_value with tuple (3 elements)", "[cpo][edge_value][tier7]") { + std::tuple edge{20, 21, 7.5}; + std::vector> el{edge}; + + auto val = edge_value(el, edge); + REQUIRE(val == 7.5); +} + +TEST_CASE("source_id with tuple (4 elements)", "[cpo][source_id][tier7]") { + std::tuple edge{22, 23, 8.5, "test"}; + std::vector> el{edge}; + + auto uid = source_id(el, edge); + REQUIRE(uid == 22); +} + +TEST_CASE("target_id with tuple (4 elements)", "[cpo][target_id][tier7]") { + std::tuple edge{24, 25, 9.5, "test"}; + std::vector> el{edge}; + + auto vid = target_id(el, edge); + REQUIRE(vid == 25); +} + +TEST_CASE("edge_value with tuple (4 elements)", "[cpo][edge_value][tier7]") { + std::tuple edge{26, 27, 10.5, "test"}; + std::vector> el{edge}; + + auto val = edge_value(el, edge); + REQUIRE(val == 10.5); +} + +// ============================================================================= +// Ambiguity Tests: Verify tier precedence +// ============================================================================= + +// Helper types for ambiguity tests - must be at namespace scope for std::get specialization +struct EdgeWithSourceAndTarget { + int source_id; + int target_id; +}; + +// Make it tuple-like via std::tuple_size and std::get specializations +namespace std { + template<> + struct tuple_size : integral_constant {}; + + template + struct tuple_element { + using type = int; + }; +} + +template +auto get(const EdgeWithSourceAndTarget& e) { + if constexpr (I == 0) return e.source_id + 100; // Different value to test precedence + else return e.target_id + 100; +} + +struct EdgeWithAllThree { + int source_id; + int target_id; + double value; +}; + +namespace std { + template<> + struct tuple_size : integral_constant {}; + + template + struct tuple_element { + using type = conditional_t; + }; +} + +template +auto get(const EdgeWithAllThree& e) { + if constexpr (I == 0) return e.source_id; + else if constexpr (I == 1) return e.target_id; + else return e.value + 100.0; // Different value +} + +TEST_CASE("source_id prefers data member over tuple", "[cpo][source_id][ambiguity]") { + // Type with both source_id data member and tuple-like interface + // Should pick data member (Tier 6) over tuple-like (Tier 7) + EdgeWithSourceAndTarget e{30, 31}; + std::vector el{e}; + + // Should use data member (30), not tuple get<0> (130) + auto uid = source_id(el, e); + REQUIRE(uid == 30); +} + +TEST_CASE("target_id prefers data member over tuple", "[cpo][target_id][ambiguity]") { + EdgeWithSourceAndTarget e{32, 33}; + std::vector el{e}; + + // Should use data member (33), not tuple get<1> (133) + auto vid = target_id(el, e); + REQUIRE(vid == 33); +} + +TEST_CASE("edge_value prefers data member over tuple", "[cpo][edge_value][ambiguity]") { + EdgeWithAllThree e{34, 35, 11.5}; + std::vector el{e}; + + // Should use data member (11.5), not tuple get<2> (111.5) + auto val = edge_value(el, e); + REQUIRE(val == 11.5); +} + +// ============================================================================= +// Noexcept Tests: Verify noexcept propagation +// ============================================================================= + +TEST_CASE("source_id with edge_info is noexcept", "[cpo][source_id][noexcept]") { + using EI = edge_info; + EI ei{40, 41}; + std::vector el{ei}; + + STATIC_REQUIRE(noexcept(source_id(el, ei))); +} + +TEST_CASE("source_id with pair is noexcept", "[cpo][source_id][noexcept]") { + std::pair edge{42, 43}; + std::vector> el{edge}; + + STATIC_REQUIRE(noexcept(source_id(el, edge))); +} + +TEST_CASE("target_id with edge_info is noexcept", "[cpo][target_id][noexcept]") { + using EI = edge_info; + EI ei{44, 45}; + std::vector el{ei}; + + STATIC_REQUIRE(noexcept(target_id(el, ei))); +} + +TEST_CASE("target_id with tuple is noexcept", "[cpo][target_id][noexcept]") { + std::tuple edge{46, 47, 12.5}; + std::vector> el{edge}; + + STATIC_REQUIRE(noexcept(target_id(el, edge))); +} + +TEST_CASE("edge_value with edge_info is noexcept", "[cpo][edge_value][noexcept]") { + using EI = edge_info; + EI ei{48, 49, 13.5}; + std::vector el{ei}; + + STATIC_REQUIRE(noexcept(edge_value(el, ei))); +} + +TEST_CASE("edge_value with tuple is noexcept", "[cpo][edge_value][noexcept]") { + std::tuple edge{50, 51, 14.5}; + std::vector> el{edge}; + + STATIC_REQUIRE(noexcept(edge_value(el, edge))); +} diff --git a/tests/edge_list/test_edge_list_descriptor.cpp b/tests/edge_list/test_edge_list_descriptor.cpp new file mode 100644 index 0000000..58231a9 --- /dev/null +++ b/tests/edge_list/test_edge_list_descriptor.cpp @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::edge_list; +using namespace graph::adj_list::_cpo_instances; + +// ============================================================================= +// Construction Tests +// ============================================================================= + +TEST_CASE("edge_list::edge_descriptor construction without value", "[edge_list][descriptor]") { + int src = 1, tgt = 2; + edge_descriptor e(src, tgt); + + REQUIRE(e.source_id() == 1); + REQUIRE(e.target_id() == 2); + REQUIRE(&e.source_id() == &src); // Verify it's a reference + REQUIRE(&e.target_id() == &tgt); // Verify it's a reference +} + +TEST_CASE("edge_list::edge_descriptor construction with value", "[edge_list][descriptor]") { + int src = 3, tgt = 4; + double val = 1.5; + edge_descriptor e(src, tgt, val); + + REQUIRE(e.source_id() == 3); + REQUIRE(e.target_id() == 4); + REQUIRE(e.value() == 1.5); + REQUIRE(&e.source_id() == &src); // Verify it's a reference + REQUIRE(&e.target_id() == &tgt); // Verify it's a reference + REQUIRE(&e.value() == &val); // Verify it's a reference +} + +TEST_CASE("edge_list::edge_descriptor deduction guides", "[edge_list][descriptor]") { + // Without value + int src1 = 5, tgt1 = 6; + auto e1 = edge_descriptor(src1, tgt1); + static_assert(std::is_same_v>); + REQUIRE(e1.source_id() == 5); + REQUIRE(e1.target_id() == 6); + + // With value + int src2 = 7, tgt2 = 8; + double val2 = 2.5; + auto e2 = edge_descriptor(src2, tgt2, val2); + static_assert(std::is_same_v>); + REQUIRE(e2.source_id() == 7); + REQUIRE(e2.target_id() == 8); + REQUIRE(e2.value() == 2.5); +} + +TEST_CASE("edge_list::edge_descriptor with string value", "[edge_list][descriptor]") { + int src = 9, tgt = 10; + std::string val = "test"; + edge_descriptor e(src, tgt, val); + + REQUIRE(e.source_id() == 9); + REQUIRE(e.target_id() == 10); + REQUIRE(e.value() == "test"); + REQUIRE(&e.value() == &val); // Verify it's a reference +} + +TEST_CASE("edge_list::edge_descriptor with string vertex IDs", "[edge_list][descriptor]") { + std::string src = "vertex_a", tgt = "vertex_b"; + double val = 1.5; + edge_descriptor e(src, tgt, val); + + REQUIRE(e.source_id() == "vertex_a"); + REQUIRE(e.target_id() == "vertex_b"); + REQUIRE(e.value() == 1.5); + + // Verify accessors return const references (no copy) + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + // Verify they're references to the original data + REQUIRE(&e.source_id() == &src); + REQUIRE(&e.target_id() == &tgt); +} + +TEST_CASE("edge_list::edge_descriptor copy constructor", "[edge_list][descriptor]") { + int src = 11, tgt = 12; + double val = 3.5; + edge_descriptor e1(src, tgt, val); + edge_descriptor e2(e1); + + REQUIRE(e2.source_id() == 11); + REQUIRE(e2.target_id() == 12); + REQUIRE(e2.value() == 3.5); + + // Both should reference the same underlying data + REQUIRE(&e1.source_id() == &e2.source_id()); + REQUIRE(&e1.target_id() == &e2.target_id()); + REQUIRE(&e1.value() == &e2.value()); +} + +TEST_CASE("edge_list::edge_descriptor move constructor", "[edge_list][descriptor]") { + int src = 13, tgt = 14; + std::string val = "moved"; + edge_descriptor e1(src, tgt, val); + edge_descriptor e2(std::move(e1)); + + REQUIRE(e2.source_id() == 13); + REQUIRE(e2.target_id() == 14); + REQUIRE(e2.value() == "moved"); + + // Both should still reference the same underlying data + REQUIRE(&e1.source_id() == &e2.source_id()); + REQUIRE(&e1.value() == &e2.value()); +} + +TEST_CASE("edge_list::edge_descriptor references underlying data", "[edge_list][descriptor]") { + std::string src = "source_vertex"; + std::string tgt = "target_vertex"; + std::string val = "edge_data"; + + edge_descriptor e(src, tgt, val); + + REQUIRE(e.source_id() == "source_vertex"); + REQUIRE(e.target_id() == "target_vertex"); + REQUIRE(e.value() == "edge_data"); + + // Modify the underlying data - descriptor should reflect the change + src = "new_source"; + REQUIRE(e.source_id() == "new_source"); + + val = "new_data"; + REQUIRE(e.value() == "new_data"); +} + +// ============================================================================= +// Trait Tests +// ============================================================================= + +TEST_CASE("is_edge_list_descriptor_v trait", "[edge_list][traits]") { + // Should be true for edge_list::edge_descriptor + static_assert(is_edge_list_descriptor_v>); + static_assert(is_edge_list_descriptor_v>); + static_assert(is_edge_list_descriptor_v>); + + // Should be false for other types + static_assert(!is_edge_list_descriptor_v); + static_assert(!is_edge_list_descriptor_v>); + static_assert(!is_edge_list_descriptor_v>); + + SUCCEED("All trait checks passed at compile time"); +} + +// ============================================================================= +// CPO Integration Tests (Tier 5) +// ============================================================================= + +TEST_CASE("source_id CPO with edge_list::edge_descriptor", "[cpo][edge_list][tier5]") { + int src = 15, tgt = 16; + edge_descriptor e(src, tgt); + std::vector> el{e}; + + auto sid = source_id(el, e); + REQUIRE(sid == 15); +} + +TEST_CASE("target_id CPO with edge_list::edge_descriptor", "[cpo][edge_list][tier5]") { + int src = 17, tgt = 18; + edge_descriptor e(src, tgt); + std::vector> el{e}; + + auto tid = target_id(el, e); + REQUIRE(tid == 18); +} + +TEST_CASE("source_id and target_id with edge_descriptor (with value)", "[cpo][edge_list][tier5]") { + int src = 19, tgt = 20; + double val = 4.5; + edge_descriptor e(src, tgt, val); + std::vector> el{e}; + + auto sid = source_id(el, e); + auto tid = target_id(el, e); + + REQUIRE(sid == 19); + REQUIRE(tid == 20); +} + +TEST_CASE("edge_value CPO with edge_list::edge_descriptor", "[cpo][edge_list][tier5]") { + int src = 21, tgt = 22; + double val = 5.5; + edge_descriptor e(src, tgt, val); + std::vector> el{e}; + + auto ev = edge_value(el, e); + REQUIRE(ev == 5.5); +} + +TEST_CASE("all CPOs with edge_list::edge_descriptor", "[cpo][edge_list][tier5]") { + int src = 23, tgt = 24; + std::string val = "edge_value"; + edge_descriptor e(src, tgt, val); + std::vector> el{e}; + + auto sid = source_id(el, e); + auto tid = target_id(el, e); + auto ev = edge_value(el, e); + + REQUIRE(sid == 23); + REQUIRE(tid == 24); + REQUIRE(ev == "edge_value"); +} + +// ============================================================================= +// Noexcept Tests +// ============================================================================= + +TEST_CASE("edge_list::edge_descriptor operations are noexcept", "[edge_list][descriptor][noexcept]") { + int src = 25, tgt = 26; + double val = 6.5; + edge_descriptor e(src, tgt, val); + + STATIC_REQUIRE(noexcept(e.source_id())); + STATIC_REQUIRE(noexcept(e.target_id())); + STATIC_REQUIRE(noexcept(e.value())); +} + +TEST_CASE("CPOs with edge_list::edge_descriptor are noexcept", "[cpo][edge_list][noexcept]") { + int src = 27, tgt = 28; + double val = 7.5; + edge_descriptor e(src, tgt, val); + std::vector> el{e}; + + STATIC_REQUIRE(noexcept(source_id(el, e))); + STATIC_REQUIRE(noexcept(target_id(el, e))); + STATIC_REQUIRE(noexcept(edge_value(el, e))); +} + +// ============================================================================= +// Comparison Tests +// ============================================================================= + +TEST_CASE("edge_list::edge_descriptor equality", "[edge_list][descriptor]") { + int src1 = 29, tgt1 = 30; + double val1 = 8.5; + edge_descriptor e1(src1, tgt1, val1); + + int src2 = 29, tgt2 = 30; + double val2 = 8.5; + edge_descriptor e2(src2, tgt2, val2); + + int src3 = 31, tgt3 = 32; + double val3 = 9.5; + edge_descriptor e3(src3, tgt3, val3); + + REQUIRE(e1 == e2); // Same values + REQUIRE(e1 != e3); // Different values +} + +TEST_CASE("edge_list::edge_descriptor ordering", "[edge_list][descriptor]") { + int src1 = 33, tgt1 = 34; + edge_descriptor e1(src1, tgt1); + + int src2 = 33, tgt2 = 35; + edge_descriptor e2(src2, tgt2); + + int src3 = 34, tgt3 = 34; + edge_descriptor e3(src3, tgt3); + + REQUIRE(e1 < e2); + REQUIRE(e1 < e3); + REQUIRE(e2 < e3); +} diff --git a/tests/edge_list/test_edge_list_integration.cpp b/tests/edge_list/test_edge_list_integration.cpp new file mode 100644 index 0000000..2e56d9e --- /dev/null +++ b/tests/edge_list/test_edge_list_integration.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include + +using namespace graph; + +// ============================================================================= +// Generic Algorithm Tests - Demonstrates unified CPO interface +// ============================================================================= + +// Generic algorithm that works with ANY edge range using unified CPOs +template + requires edge_list::basic_sourced_edgelist +auto count_self_loops(EdgeRange&& edges) { + int count = 0; + for (auto&& uv : edges) { + auto src = adj_list::_cpo_instances::source_id(edges, uv); + auto tgt = adj_list::_cpo_instances::target_id(edges, uv); + if (src == tgt) { + ++count; + } + } + return count; +} + +// Generic algorithm that sums edge values (for edge lists with values) +template + requires edge_list::has_edge_value +auto sum_edge_values(EdgeRange&& edges) { + using value_t = edge_list::edge_value_t; + value_t sum = 0; + for (auto&& uv : edges) { + sum += adj_list::_cpo_instances::edge_value(edges, uv); + } + return sum; +} + +TEST_CASE("Generic algorithm works with std::pair", "[integration][algorithm]") { + std::vector> pairs{{1, 2}, {3, 3}, {4, 4}, {5, 6}}; + REQUIRE(count_self_loops(pairs) == 2); +} + +TEST_CASE("Generic algorithm works with std::tuple (2-element)", "[integration][algorithm]") { + std::vector> tuples{{1, 1}, {2, 3}, {4, 4}}; + REQUIRE(count_self_loops(tuples) == 2); +} + +TEST_CASE("Generic algorithm works with std::tuple (3-element)", "[integration][algorithm]") { + std::vector> tuples{ + {1, 2, 1.5}, + {3, 3, 2.5}, + {4, 5, 3.5} + }; + REQUIRE(count_self_loops(tuples) == 1); +} + +TEST_CASE("Generic algorithm works with edge_info (no value)", "[integration][algorithm]") { + using EI = edge_info; + std::vector infos{{1, 2}, {5, 5}, {7, 8}}; + REQUIRE(count_self_loops(infos) == 1); +} + +TEST_CASE("Generic algorithm works with edge_info (with value)", "[integration][algorithm]") { + using EI = edge_info; + std::vector infos{ + {1, 2, 10.0}, + {3, 3, 20.0}, + {4, 4, 30.0} + }; + REQUIRE(count_self_loops(infos) == 2); +} + +TEST_CASE("Generic algorithm works with edge_list::edge_descriptor (no value)", "[integration][algorithm]") { + int s1 = 1, t1 = 1; + int s2 = 2, t2 = 3; + int s3 = 4, t3 = 4; + + edge_list::edge_descriptor e1(s1, t1); + edge_list::edge_descriptor e2(s2, t2); + edge_list::edge_descriptor e3(s3, t3); + + std::vector> descs{e1, e2, e3}; + REQUIRE(count_self_loops(descs) == 2); +} + +TEST_CASE("Generic algorithm works with edge_list::edge_descriptor (with value)", "[integration][algorithm]") { + int s1 = 5, t1 = 5; + int s2 = 6, t2 = 7; + double v1 = 1.1, v2 = 2.2; + + edge_list::edge_descriptor e1(s1, t1, v1); + edge_list::edge_descriptor e2(s2, t2, v2); + + std::vector> descs{e1, e2}; + REQUIRE(count_self_loops(descs) == 1); +} + +// ============================================================================= +// Edge Value Algorithm Tests +// ============================================================================= + +TEST_CASE("sum_edge_values works with 3-tuples", "[integration][edge_value]") { + std::vector> tuples{ + {1, 2, 1.5}, + {2, 3, 2.5}, + {3, 4, 3.0} + }; + REQUIRE(sum_edge_values(tuples) == 7.0); +} + +TEST_CASE("sum_edge_values works with edge_info", "[integration][edge_value]") { + using EI = edge_info; + std::vector infos{ + {1, 2, 10}, + {2, 3, 20}, + {3, 4, 30} + }; + REQUIRE(sum_edge_values(infos) == 60); +} + +TEST_CASE("sum_edge_values works with edge_list::edge_descriptor", "[integration][edge_value]") { + int s1 = 1, t1 = 2, s2 = 2, t2 = 3; + double v1 = 5.5, v2 = 4.5; + + edge_list::edge_descriptor e1(s1, t1, v1); + edge_list::edge_descriptor e2(s2, t2, v2); + + std::vector> descs{e1, e2}; + REQUIRE(sum_edge_values(descs) == 10.0); +} + +// ============================================================================= +// Mixed Edge Types in Same Compilation Unit +// ============================================================================= + +TEST_CASE("Different edge types work together in same compilation unit", "[integration][mixed]") { + // Pairs + std::vector> pairs{{1, 1}, {2, 3}}; + + // Tuples + std::vector> tuples{ + {4, 4, 1.0}, + {5, 6, 2.0} + }; + + // edge_info + using EI = edge_info; + std::vector infos{{7, 8}, {9, 9}}; + + // edge_descriptors + int s1 = 10, t1 = 10; + edge_list::edge_descriptor ed(s1, t1); + std::vector> descs{ed}; + + // All work with the same algorithm + REQUIRE(count_self_loops(pairs) == 1); + REQUIRE(count_self_loops(tuples) == 1); + REQUIRE(count_self_loops(infos) == 1); + REQUIRE(count_self_loops(descs) == 1); + + // Total self-loops + REQUIRE(count_self_loops(pairs) + count_self_loops(tuples) + + count_self_loops(infos) + count_self_loops(descs) == 4); +} + +// ============================================================================= +// String Vertex IDs (Non-Integral) +// ============================================================================= + +TEST_CASE("Generic algorithm works with string vertex IDs", "[integration][strings]") { + std::vector> edges{ + {"Alice", "Bob"}, + {"Bob", "Bob"}, + {"Charlie", "Dave"}, + {"Eve", "Eve"} + }; + + REQUIRE(count_self_loops(edges) == 2); +} + +TEST_CASE("Type aliases work correctly with string vertex IDs", "[integration][strings]") { + using edge_list_type = std::vector>; + + using vid = edge_list::vertex_id_t; + using edge = edge_list::edge_t; + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v>); +} + +// ============================================================================= +// Concept Satisfaction Verification +// ============================================================================= + +TEST_CASE("All edge types satisfy basic_sourced_edgelist", "[integration][concepts]") { + STATIC_REQUIRE(edge_list::basic_sourced_edgelist>>); + STATIC_REQUIRE(edge_list::basic_sourced_edgelist>>); + STATIC_REQUIRE(edge_list::basic_sourced_edgelist>>); + STATIC_REQUIRE(edge_list::basic_sourced_edgelist>>); + STATIC_REQUIRE(edge_list::basic_sourced_edgelist>>); +} + +TEST_CASE("Integral types satisfy basic_sourced_index_edgelist", "[integration][concepts]") { + STATIC_REQUIRE(edge_list::basic_sourced_index_edgelist>>); + STATIC_REQUIRE(!edge_list::basic_sourced_index_edgelist>>); +} + +TEST_CASE("Valued edge types satisfy has_edge_value", "[integration][concepts]") { + STATIC_REQUIRE(!edge_list::has_edge_value>>); + STATIC_REQUIRE(!edge_list::has_edge_value>>); + STATIC_REQUIRE(edge_list::has_edge_value>>); + STATIC_REQUIRE(!edge_list::has_edge_value>>); + STATIC_REQUIRE(edge_list::has_edge_value>>); + STATIC_REQUIRE(!edge_list::has_edge_value>>); + STATIC_REQUIRE(edge_list::has_edge_value>>); +} diff --git a/tests/views/CMakeLists.txt b/tests/views/CMakeLists.txt new file mode 100644 index 0000000..c98e090 --- /dev/null +++ b/tests/views/CMakeLists.txt @@ -0,0 +1,28 @@ +add_executable(graph3_views_tests + test_main.cpp + test_vertex_info.cpp + test_edge_info.cpp + test_neighbor_info.cpp + test_search_base.cpp + test_view_concepts.cpp + test_vertexlist.cpp + test_incidence.cpp + test_neighbors.cpp + test_edgelist.cpp + test_basic_views.cpp + test_dfs.cpp + test_bfs.cpp + test_topological_sort.cpp + test_adaptors.cpp + test_unified_header.cpp + test_graph_hpp_includes_views.cpp + test_edge_cases.cpp +) + +target_link_libraries(graph3_views_tests + PRIVATE + graph3 + Catch2::Catch2 +) + +add_test(NAME views_tests COMMAND graph3_views_tests) diff --git a/tests/views/test_adaptors.cpp b/tests/views/test_adaptors.cpp new file mode 100644 index 0000000..de9cc51 --- /dev/null +++ b/tests/views/test_adaptors.cpp @@ -0,0 +1,960 @@ +/** + * @file test_adaptors.cpp + * @brief Tests for range adaptor closures enabling pipe syntax + */ + +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views::adaptors; // Use adaptor objects for pipe syntax +using std::ranges::size; + +// Simple test graph using vector-of-vectors +// Graph structure: 0 -> {1, 2}, 1 -> {2}, 2 -> {} +using test_graph = std::vector>; + +auto make_test_graph() { + test_graph g(3); // 3 vertices + g[0] = {1, 2}; // vertex 0 connects to vertices 1 and 2 + g[1] = {2}; // vertex 1 connects to vertex 2 + g[2] = {}; // vertex 2 has no outgoing edges + return g; +} + +//============================================================================= +// vertexlist adaptor tests +//============================================================================= + +TEST_CASE("vertexlist adaptor - basic pipe syntax", "[adaptors][vertexlist]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | vertexlist() + auto view = g | vertexlist(); + + REQUIRE(size(view) == 3); + + std::vector vertex_ids; + for (auto [v] : view) { + vertex_ids.push_back(vertex_id(g, v)); + } + + REQUIRE(vertex_ids == std::vector{0, 1, 2}); +} + +TEST_CASE("vertexlist adaptor - with value function", "[adaptors][vertexlist]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | vertexlist(vvf) + auto vvf = [&g](auto&& v) { return vertex_id(g, v) * 10; }; // Simple transform + auto view = g | vertexlist(vvf); + + REQUIRE(size(view) == 3); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{0, 10, 20}); +} + +TEST_CASE("vertexlist adaptor - chaining with std::views::take", "[adaptors][vertexlist]") { + auto g = make_test_graph(); + + // Test chaining: g | vertexlist() | std::views::take(2) + auto view = g | vertexlist() | std::views::take(2); + + std::vector vertex_ids; + for (auto [v] : view) { + vertex_ids.push_back(vertex_id(g, v)); + } + + REQUIRE(vertex_ids.size() == 2); + REQUIRE(vertex_ids == std::vector{0, 1}); +} + +TEST_CASE("vertexlist adaptor - chaining with transform", "[adaptors][vertexlist]") { + auto g = make_test_graph(); + + // Test chaining pattern: g | vertexlist() | std::views::transform | std::views::filter + // This pattern works because vertexlist() (without VVF) creates a semiregular view + auto view = g | vertexlist() + | std::views::transform([&g](auto&& tuple) { + auto [v] = tuple; + return std::make_tuple(v, vertex_id(g, v) * 10); + }) + | std::views::filter([](auto&& tuple) { + auto [v, val] = tuple; + return val > 0; + }); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{10, 20}); +} + +TEST_CASE("vertexlist adaptor - direct call compatibility", "[adaptors][vertexlist]") { + auto g = make_test_graph(); + + // Test that adaptor can be called directly (not just piped) + auto view1 = graph::views::vertexlist(g); + auto view2 = g | vertexlist(); + + REQUIRE(size(view1) == size(view2)); + REQUIRE(size(view1) == 3); +} + +//============================================================================= +// incidence adaptor tests +//============================================================================= + +TEST_CASE("incidence adaptor - basic pipe syntax", "[adaptors][incidence]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | incidence(uid) + auto view = g | incidence(0); + + REQUIRE(size(view) == 2); // vertex 0 has 2 outgoing edges + + std::vector target_ids; + for (auto [e] : view) { + target_ids.push_back(vertex_id(g, target(g, e))); + } + + REQUIRE(target_ids == std::vector{1, 2}); +} + +TEST_CASE("incidence adaptor - with value function", "[adaptors][incidence]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | incidence(uid, evf) + auto evf = [&g](auto&& e) { return target_id(g, e) * 10; }; // Transform target ID + auto view = g | incidence(0, evf); + + REQUIRE(size(view) == 2); + + std::vector values; + for (auto [e, val] : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{10, 20}); // targets 1 and 2 +} + +TEST_CASE("incidence adaptor - chaining with std::views::take", "[adaptors][incidence]") { + auto g = make_test_graph(); + + // Test chaining: g | incidence(uid) | std::views::take(1) + auto view = g | incidence(0) | std::views::take(1); + + std::vector target_ids; + for (auto [e] : view) { + target_ids.push_back(vertex_id(g, target(g, e))); + } + + REQUIRE(target_ids.size() == 1); + REQUIRE(target_ids == std::vector{1}); +} + +TEST_CASE("incidence adaptor - chaining with transform", "[adaptors][incidence]") { + auto g = make_test_graph(); + + // Test chaining pattern: g | incidence(uid) | std::views::transform + // This pattern works because incidence(uid) (without EVF) creates a semiregular view + auto view = g | incidence(0) + | std::views::transform([&g](auto&& tuple) { + auto [e] = tuple; + return target_id(g, e) * 10; + }) + | std::views::transform([](auto val) { + return val * 2; + }); + + std::vector values; + for (auto val : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{20, 40}); // (1*10*2, 2*10*2) +} + +TEST_CASE("incidence adaptor - direct call compatibility", "[adaptors][incidence]") { + auto g = make_test_graph(); + + // Test that adaptor can be called directly (not just piped) + auto view1 = graph::views::incidence(g, 0); + auto view2 = g | incidence(0); + + REQUIRE(size(view1) == size(view2)); + REQUIRE(size(view1) == 2); +} + +//============================================================================= +// neighbors adaptor tests +//============================================================================= + +TEST_CASE("neighbors adaptor - basic pipe syntax", "[adaptors][neighbors]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | neighbors(uid) + auto view = g | neighbors(0); + + REQUIRE(size(view) == 2); // vertex 0 has 2 neighbors + + std::vector neighbor_ids; + for (auto [v] : view) { + neighbor_ids.push_back(vertex_id(g, v)); + } + + REQUIRE(neighbor_ids == std::vector{1, 2}); +} + +TEST_CASE("neighbors adaptor - with value function", "[adaptors][neighbors]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | neighbors(uid, vvf) + auto vvf = [&g](auto&& v) { return vertex_id(g, v) * 10; }; + auto view = g | neighbors(0, vvf); + + REQUIRE(size(view) == 2); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{10, 20}); +} + +TEST_CASE("neighbors adaptor - chaining with std::views::filter", "[adaptors][neighbors]") { + auto g = make_test_graph(); + + // Test chaining: g | neighbors(uid) | std::views::filter + auto view = g | neighbors(0) | std::views::filter([&g](auto&& tuple) { + auto [v] = tuple; + return vertex_id(g, v) > 1; + }); + + std::vector neighbor_ids; + for (auto [v] : view) { + neighbor_ids.push_back(vertex_id(g, v)); + } + + REQUIRE(neighbor_ids == std::vector{2}); +} + +TEST_CASE("neighbors adaptor - direct call compatibility", "[adaptors][neighbors]") { + auto g = make_test_graph(); + + // Test that adaptor can be called directly (not just piped) + auto view1 = graph::views::neighbors(g, 0); + auto view2 = g | neighbors(0); + + REQUIRE(size(view1) == size(view2)); + REQUIRE(size(view1) == 2); +} + +//============================================================================= +// edgelist adaptor tests +//============================================================================= + +TEST_CASE("edgelist adaptor - basic pipe syntax", "[adaptors][edgelist]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | edgelist() + auto view = g | edgelist(); + + std::vector> edge_pairs; + for (auto [e] : view) { + edge_pairs.emplace_back(vertex_id(g, source(g, e)), vertex_id(g, target(g, e))); + } + + REQUIRE(edge_pairs.size() == 3); // 3 edges total + REQUIRE(edge_pairs == std::vector>{{0, 1}, {0, 2}, {1, 2}}); +} + +TEST_CASE("edgelist adaptor - with value function", "[adaptors][edgelist]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | edgelist(evf) + auto evf = [&g](auto&& e) { return target_id(g, e) * 10; }; + auto view = g | edgelist(evf); + + std::vector values; + for (auto [e, val] : view) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + REQUIRE(values == std::vector{10, 20, 20}); // targets are 1, 2, 2 +} + +TEST_CASE("edgelist adaptor - chaining with std::views::take", "[adaptors][edgelist]") { + auto g = make_test_graph(); + + // Test chaining: g | edgelist() | std::views::take(2) + auto view = g | edgelist() | std::views::take(2); + + std::vector> edge_pairs; + for (auto [e] : view) { + edge_pairs.emplace_back(vertex_id(g, source(g, e)), vertex_id(g, target(g, e))); + } + + REQUIRE(edge_pairs.size() == 2); + REQUIRE(edge_pairs == std::vector>{{0, 1}, {0, 2}}); +} + +TEST_CASE("edgelist adaptor - chaining with transform and filter", "[adaptors][edgelist]") { + auto g = make_test_graph(); + + // Test chaining pattern: g | edgelist() | std::views::transform | std::views::filter + // This pattern works because edgelist() (without EVF) creates a semiregular view + auto view = g | edgelist() + | std::views::transform([&g](auto&& tuple) { + auto [e] = tuple; + return std::make_tuple(e, target_id(g, e) * 10); + }) + | std::views::filter([](auto&& tuple) { + auto [e, val] = tuple; + return val >= 20; + }); + + std::vector values; + for (auto [e, val] : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{20, 20}); // Two edges with target ID 2 +} + +TEST_CASE("edgelist adaptor - direct call compatibility", "[adaptors][edgelist]") { + auto g = make_test_graph(); + + // Test that adaptor can be called directly (not just piped) + auto view1 = graph::views::edgelist(g); + auto view2 = g | edgelist(); + + // Count elements by iteration + size_t count1 = 0, count2 = 0; + for (auto [e] : view1) { ++count1; } + for (auto [e] : view2) { ++count2; } + + REQUIRE(count1 == count2); + REQUIRE(count1 == 3); +} + +//============================================================================= +// Multi-adaptor composition tests +//============================================================================= + +TEST_CASE("multiple views can be used independently with pipe syntax", "[adaptors][composition]") { + auto g = make_test_graph(); + + // Use multiple different adaptors on same graph + auto vertices = g | vertexlist(); + auto edges_from_0 = g | incidence(0); + auto neighbors_of_0 = g | neighbors(0); + auto all_edges = g | edgelist(); + + REQUIRE(size(vertices) == 3); + REQUIRE(size(edges_from_0) == 2); + REQUIRE(size(neighbors_of_0) == 2); + + // Count all_edges by iteration (edgelist may not be sized_range) + size_t count = 0; + for (auto [e] : all_edges) { ++count; } + REQUIRE(count == 3); +} + +TEST_CASE("adaptors work with std::views algorithms", "[adaptors][composition]") { + auto g = make_test_graph(); + + // Test that views compose well with standard algorithms + // Pattern: use vertexlist() then std::views::transform for value computation + auto view = g | vertexlist() + | std::views::transform([&g](auto&& tuple) { + auto [v] = tuple; + return std::make_tuple(v, vertex_id(g, v) * 10); + }) + | std::views::filter([](auto&& tuple) { + auto [v, val] = tuple; + return val >= 10; + }); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + REQUIRE(values == std::vector{10, 20}); +} + +TEST_CASE("complex chaining scenario", "[adaptors][composition]") { + auto g = make_test_graph(); + + // Complex chain: get vertices, compute values, take first 2, transform values + // Pattern: use vertexlist() then std::views::transform for value computation + auto view = g | vertexlist() + | std::views::transform([&g](auto&& tuple) { + auto [v] = tuple; + return vertex_id(g, v) * 10; + }) + | std::views::take(2) + | std::views::transform([](auto val) { + return val + 1; + }); + + std::vector results; + for (auto val : view) { + results.push_back(val); + } + + REQUIRE(results == std::vector{1, 11}); +} + +//============================================================================= +// Search view adaptor tests +//============================================================================= + +TEST_CASE("vertices_dfs adaptor - basic pipe syntax", "[adaptors][dfs][vertices_dfs]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | vertices_dfs(seed) + auto view = g | vertices_dfs(0); + + std::vector visited; + for (auto [v] : view) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 3); // All vertices reachable + REQUIRE(visited[0] == 0); // Starts at seed +} + +TEST_CASE("vertices_dfs adaptor - with value function", "[adaptors][dfs][vertices_dfs]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | vertices_dfs(seed, vvf) + auto vvf = [&g](auto&& v) { return vertex_id(g, v) * 10; }; + auto view = g | vertices_dfs(0, vvf); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + REQUIRE(values[0] == 0); // First vertex has value 0*10=0 +} + +TEST_CASE("vertices_dfs adaptor - chaining with std::views", "[adaptors][dfs][vertices_dfs]") { + auto g = make_test_graph(); + + // Test chaining: g | vertices_dfs(seed) | std::views::transform + auto view = g | vertices_dfs(0) + | std::views::transform([&g](auto&& tuple) { + auto [v] = tuple; + return vertex_id(g, v); + }); + + std::vector visited; + for (auto id : view) { + visited.push_back(id); + } + + REQUIRE(visited.size() == 3); + REQUIRE(visited[0] == 0); // Starts at seed +} + +TEST_CASE("edges_dfs adaptor - basic pipe syntax", "[adaptors][dfs][edges_dfs]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | edges_dfs(seed) + auto view = g | edges_dfs(0); + + std::vector> edges; + for (auto [e] : view) { + edges.emplace_back(vertex_id(g, source(g, e)), vertex_id(g, target(g, e))); + } + + REQUIRE(edges.size() >= 2); // At least 2 edges traversed + // First edge should be from seed vertex 0 + REQUIRE((edges[0].first == 0 || edges[1].first == 0)); +} + +TEST_CASE("edges_dfs adaptor - with value function", "[adaptors][dfs][edges_dfs]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | edges_dfs(seed, evf) + auto evf = [&g](auto&& e) { return target_id(g, e) * 10; }; + auto view = g | edges_dfs(0, evf); + + std::vector values; + for (auto [e, val] : view) { + values.push_back(val); + } + + REQUIRE(values.size() >= 2); + // Values should be multiples of 10 + for (auto val : values) { + REQUIRE(val % 10 == 0); + } +} + +TEST_CASE("vertices_bfs adaptor - basic pipe syntax", "[adaptors][bfs][vertices_bfs]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | vertices_bfs(seed) + auto view = g | vertices_bfs(0); + + std::vector visited; + for (auto [v] : view) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 3); // All vertices reachable + REQUIRE(visited[0] == 0); // Starts at seed +} + +TEST_CASE("vertices_bfs adaptor - with value function", "[adaptors][bfs][vertices_bfs]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | vertices_bfs(seed, vvf) + auto vvf = [&g](auto&& v) { return vertex_id(g, v) * 10; }; + auto view = g | vertices_bfs(0, vvf); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + REQUIRE(values[0] == 0); // First vertex has value 0*10=0 +} + +TEST_CASE("vertices_bfs adaptor - chaining with std::views", "[adaptors][bfs][vertices_bfs]") { + auto g = make_test_graph(); + + // Test chaining: g | vertices_bfs(seed) | std::views::filter + auto view = g | vertices_bfs(0) + | std::views::transform([&g](auto&& tuple) { + auto [v] = tuple; + return vertex_id(g, v); + }) + | std::views::filter([](int id) { + return id > 0; + }); + + std::vector visited; + for (auto id : view) { + visited.push_back(id); + } + + REQUIRE(visited.size() == 2); // Only vertices 1 and 2 + REQUIRE(std::ranges::all_of(visited, [](int id) { return id > 0; })); +} + +TEST_CASE("edges_bfs adaptor - basic pipe syntax", "[adaptors][bfs][edges_bfs]") { + auto g = make_test_graph(); + + // Test basic pipe syntax: g | edges_bfs(seed) + auto view = g | edges_bfs(0); + + std::vector> edges; + for (auto [e] : view) { + edges.emplace_back(vertex_id(g, source(g, e)), vertex_id(g, target(g, e))); + } + + REQUIRE(edges.size() >= 2); // At least 2 edges traversed + // First edges should be from seed vertex 0 + REQUIRE(edges[0].first == 0); +} + +TEST_CASE("edges_bfs adaptor - with value function", "[adaptors][bfs][edges_bfs]") { + auto g = make_test_graph(); + + // Test pipe syntax with value function: g | edges_bfs(seed, evf) + auto evf = [&g](auto&& e) { return target_id(g, e) * 10; }; + auto view = g | edges_bfs(0, evf); + + std::vector values; + for (auto [e, val] : view) { + values.push_back(val); + } + + REQUIRE(values.size() >= 2); + // Values should be multiples of 10 + for (auto val : values) { + REQUIRE(val % 10 == 0); + } +} + +TEST_CASE("search adaptors - direct call compatibility", "[adaptors][dfs][bfs]") { + auto g = make_test_graph(); + + // Test that adaptors can be called directly (not just piped) + auto dfs_view1 = graph::views::vertices_dfs(g, 0); + auto dfs_view2 = g | vertices_dfs(0); + + std::vector visited1, visited2; + for (auto [v] : dfs_view1) visited1.push_back(vertex_id(g, v)); + for (auto [v] : dfs_view2) visited2.push_back(vertex_id(g, v)); + + REQUIRE(visited1 == visited2); + + // Test BFS as well + auto bfs_view1 = graph::views::vertices_bfs(g, 0); + auto bfs_view2 = g | vertices_bfs(0); + + visited1.clear(); + visited2.clear(); + for (auto [v] : bfs_view1) visited1.push_back(vertex_id(g, v)); + for (auto [v] : bfs_view2) visited2.push_back(vertex_id(g, v)); + + REQUIRE(visited1 == visited2); +} + +TEST_CASE("vertices_topological_sort adaptor - basic pipe syntax", "[adaptors][topological_sort]") { + auto g = make_test_graph(); + + // Use pipe syntax + std::vector vertices; + for (auto [v] : g | vertices_topological_sort()) { + vertices.push_back(vertex_id(g, v)); + } + + // Should visit all vertices + REQUIRE(vertices.size() == num_vertices(g)); + + // Check topological order property: for each edge (u,v), u comes after v in the order + // (reverse post-order means sources come after targets) + std::unordered_map pos; + for (size_t i = 0; i < vertices.size(); ++i) { + pos[vertices[i]] = i; + } + + for (auto [e] : g | edgelist()) { + auto src = vertex_id(g, source(g, e)); + auto tgt = vertex_id(g, target(g, e)); + // In topological sort, each vertex appears before all vertices it has edges to + // so source comes before target: pos[src] < pos[tgt] + REQUIRE(pos[src] < pos[tgt]); + } +} + +TEST_CASE("vertices_topological_sort adaptor - with value function", "[adaptors][topological_sort]") { + auto g = make_test_graph(); + + auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; + + std::vector> results; + for (auto [v, val] : g | vertices_topological_sort(vvf)) { + results.push_back({vertex_id(g, v), val}); + } + + REQUIRE(results.size() == num_vertices(g)); + + // Check that value function was applied correctly + for (auto [vid, val] : results) { + REQUIRE(val == vid * 10); + } +} + +TEST_CASE("edges_topological_sort adaptor - basic pipe syntax", "[adaptors][topological_sort]") { + auto g = make_test_graph(); + + std::vector> edges; + for (auto [e] : g | edges_topological_sort()) { + auto src = vertex_id(g, source(g, e)); + auto tgt = vertex_id(g, target(g, e)); + edges.push_back({src, tgt}); + } + + REQUIRE(edges.size() == num_edges(g)); +} + +TEST_CASE("edges_topological_sort adaptor - with value function", "[adaptors][topological_sort]") { + auto g = make_test_graph(); + + auto evf = [&g](auto e) { return vertex_id(g, source(g, e)) + vertex_id(g, target(g, e)); }; + + std::vector values; + for (auto [e, val] : g | edges_topological_sort(evf)) { + values.push_back(val); + } + + REQUIRE(values.size() == num_edges(g)); + // Each value should be sum of source and target vertex IDs + for (auto val : values) { + REQUIRE(val >= 0); // Basic sanity check + } +} + +TEST_CASE("topological_sort adaptors - chaining with std::views", "[adaptors][topological_sort]") { + auto g = make_test_graph(); + + // Chain with transform to extract just the vertex IDs + std::vector ids; + for (auto id : g | vertices_topological_sort() + | std::views::transform([&g](auto tup) { + auto [v] = tup; + return vertex_id(g, v); + })) { + ids.push_back(id); + } + + REQUIRE(ids.size() == num_vertices(g)); +} + +TEST_CASE("topological_sort adaptors - direct call compatibility", "[adaptors][topological_sort]") { + auto g = make_test_graph(); + + // Test that adaptors can be called directly (not just piped) + auto topo_view1 = graph::views::vertices_topological_sort(g); + auto topo_view2 = g | vertices_topological_sort(); + + std::vector visited1, visited2; + for (auto [v] : topo_view1) visited1.push_back(vertex_id(g, v)); + for (auto [v] : topo_view2) visited2.push_back(vertex_id(g, v)); + + REQUIRE(visited1 == visited2); +} +//============================================================================= +// Comprehensive chaining tests (Step 6.3) +//============================================================================= + +TEST_CASE("complex chaining - multiple transforms", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Chain multiple transforms together + std::vector results; + for (auto id : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::transform([](int id) { return id * 10; }) + | std::views::transform([](int val) { return val + 5; })) { + results.push_back(id); + } + + REQUIRE(results.size() == 3); + // Each result should be (id * 10) + 5 + REQUIRE(results[0] == 5); // (0 * 10) + 5 + REQUIRE(results[1] == 15); // (1 * 10) + 5 + REQUIRE(results[2] == 25); // (2 * 10) + 5 +} + +TEST_CASE("complex chaining - filter and transform", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Filter vertices, then transform the results + std::vector results; + for (auto val : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::filter([](int id) { return id > 0; }) + | std::views::transform([](int id) { return id * 100; })) { + results.push_back(val); + } + + REQUIRE(results.size() == 2); + REQUIRE(results[0] == 100); // vertex 1 + REQUIRE(results[1] == 200); // vertex 2 +} + +TEST_CASE("complex chaining - transform, filter, transform", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // More complex chain: transform -> filter -> transform + std::vector results; + for (auto val : g | edgelist() + | std::views::transform([&g](auto info) { + auto [e] = info; + return vertex_id(g, target(g, e)); + }) + | std::views::filter([](int tgt) { return tgt == 2; }) + | std::views::transform([](int id) { return id * 7; })) { + results.push_back(val); + } + + // Edges: 0->1, 0->2, 1->2. After filter (tgt==2): 0->2, 1->2 + REQUIRE(results.size() == 2); + REQUIRE(results[0] == 14); // 2 * 7 + REQUIRE(results[1] == 14); // 2 * 7 +} + +TEST_CASE("chaining with std::views::take", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Take first N elements from a view + std::vector results; + for (auto val : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::take(2)) { + results.push_back(val); + } + + REQUIRE(results.size() == 2); +} + +TEST_CASE("chaining with std::views::drop", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Drop first N elements from a view + std::vector results; + for (auto val : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::drop(1)) { + results.push_back(val); + } + + REQUIRE(results.size() == 2); + // After dropping first element, should have vertices 1 and 2 +} + +TEST_CASE("chaining incidence with transforms", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Complex chain with incidence view + std::vector results; + for (auto val : g | incidence(0) + | std::views::transform([&g](auto info) { + auto [e] = info; + return vertex_id(g, target(g, e)); + }) + | std::views::filter([](int tgt) { return tgt < 2; }) + | std::views::transform([](int id) { return id * 3; })) { + results.push_back(val); + } + + // Vertex 0's edges: 0->1, 0->2 + // After filter (tgt < 2): only 0->1 + REQUIRE(results.size() == 1); + REQUIRE(results[0] == 3); // 1 * 3 +} + +TEST_CASE("chaining neighbors with filter", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Chain neighbors view with filter + std::vector results; + for (auto id : g | neighbors(0) + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::filter([](int id) { return id % 2 == 0; })) { + results.push_back(id); + } + + // Neighbors of 0: {1, 2}, filter even: {2} + REQUIRE(results.size() == 1); + REQUIRE(results[0] == 2); +} + +TEST_CASE("const correctness - const graph with pipe", "[adaptors][const]") { + const auto g = make_test_graph(); + + // Test that views work with const graphs + std::vector results; + for (auto [v] : g | vertexlist()) { + results.push_back(vertex_id(g, v)); + } + + REQUIRE(results.size() == 3); +} + +TEST_CASE("const correctness - const graph with chaining", "[adaptors][const]") { + const auto g = make_test_graph(); + + // Test chaining with const graph + std::vector results; + for (auto id : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::filter([](int id) { return id < 3; })) { + results.push_back(id); + } + + REQUIRE(results.size() == 3); +} + +TEST_CASE("mixing different view types in chains", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Get all neighbors of all vertices using chains + std::vector all_neighbors; + for (auto vid : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + })) { + // For each vertex, get its neighbors + for (auto [n] : g | neighbors(vid)) { + all_neighbors.push_back(vertex_id(g, n)); + } + } + + // 0->{1,2}, 1->{2}, 2->{} + REQUIRE(all_neighbors.size() == 3); +} + +TEST_CASE("search views - complex chaining with multiple filters", "[adaptors][chaining][dfs]") { + auto g = make_test_graph(); + + // Complex chain with DFS + std::vector results; + for (auto id : g | vertices_dfs(0) + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::filter([](int id) { return id >= 0; }) + | std::views::filter([](int id) { return id < 10; }) + | std::views::transform([](int id) { return id + 1; })) { + results.push_back(id); + } + + // All 3 vertices pass both filters, then +1 applied + REQUIRE(results.size() == 3); + REQUIRE(results[0] == 1); // 0 + 1 +} + +TEST_CASE("edgelist chaining with reverse", "[adaptors][chaining]") { + auto g = make_test_graph(); + + // Collect edges, reverse the order + std::vector> edges; + auto edge_view = g | edgelist() + | std::views::transform([&g](auto info) { + auto [e] = info; + return std::make_pair( + vertex_id(g, source(g, e)), + vertex_id(g, target(g, e)) + ); + }); + + for (auto edge : edge_view) { + edges.push_back(edge); + } + + REQUIRE(edges.size() == 3); +} \ No newline at end of file diff --git a/tests/views/test_basic_views.cpp b/tests/views/test_basic_views.cpp new file mode 100644 index 0000000..bfcb74c --- /dev/null +++ b/tests/views/test_basic_views.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +// Test that basic_views.hpp includes all basic view headers and compiles cleanly +TEST_CASE("basic_views.hpp header compilation", "[basic_views][header]") { + // Verify the header compiles and provides access to edge_list views + + SECTION("edgelist view for edge_list is accessible") { + std::vector> el = {{0, 1}, {1, 2}, {2, 3}}; + auto elist = graph::views::edgelist(el); + REQUIRE(std::ranges::distance(elist) == 3); + } + + SECTION("edgelist view with value function is accessible") { + std::vector> el = {{0, 1, 1.5}, {1, 2, 2.5}}; + // Note: EVF for edge_list receives (EL&, edge) by value + auto elist = graph::views::edgelist(el, [](auto& /*el*/, auto e) { + return std::get<2>(e); + }); + REQUIRE(std::ranges::distance(elist) == 2); + } +} + +// Verify that including basic_views.hpp makes all individual view headers available +TEST_CASE("basic_views.hpp includes expected headers", "[basic_views][header]") { + // These tests just verify the expected namespaces and functions exist + // after including basic_views.hpp + + SECTION("graph::views namespace is accessible") { + // Verify some expected functions are in scope + std::vector> el = {{0, 1}}; + [[maybe_unused]] auto elist = graph::views::edgelist(el); + SUCCEED("graph::views::edgelist accessible"); + } +} diff --git a/tests/views/test_bfs.cpp b/tests/views/test_bfs.cpp new file mode 100644 index 0000000..225a43a --- /dev/null +++ b/tests/views/test_bfs.cpp @@ -0,0 +1,713 @@ +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +TEST_CASE("vertices_bfs - basic traversal", "[bfs][vertices]") { + // Create simple tree: 0 -> [1, 2], 1 -> [3, 4] + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {} // 4 (leaf) + }; + + std::vector visited; + for (auto [v] : vertices_bfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited.size() == 5); + REQUIRE(visited[0] == 0); // Root + // Level 1: 1, 2 (order may vary) + REQUIRE(std::find(visited.begin() + 1, visited.begin() + 3, 1) != visited.begin() + 3); + REQUIRE(std::find(visited.begin() + 1, visited.begin() + 3, 2) != visited.begin() + 3); + // Level 2: 3, 4 (order may vary) + REQUIRE(std::find(visited.begin() + 3, visited.end(), 3) != visited.end()); + REQUIRE(std::find(visited.begin() + 3, visited.end(), 4) != visited.end()); +} + +TEST_CASE("vertices_bfs - level order", "[bfs][vertices]") { + using Graph = std::vector>; + // Create tree with distinct levels + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {5}, // 2 -> 5 + {}, // 3 (leaf) + {}, // 4 (leaf) + {} // 5 (leaf) + }; + + std::vector visited; + for (auto [v] : vertices_bfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Verify level-order traversal + REQUIRE(visited.size() == 6); + REQUIRE(visited[0] == 0); // Level 0 + // Level 1 (1 and 2 in some order) + REQUIRE(((visited[1] == 1 && visited[2] == 2) || (visited[1] == 2 && visited[2] == 1))); + // Level 2 (3, 4, 5 in some order, but all after level 1) + std::set level2(visited.begin() + 3, visited.end()); + REQUIRE(level2 == std::set{3, 4, 5}); +} + +TEST_CASE("vertices_bfs - structured bindings", "[bfs][vertices]") { + using Graph = std::vector>; + Graph g = { + {1}, // 0 -> 1 + {} // 1 (leaf) + }; + + int count = 0; + for (auto [v] : vertices_bfs(g, 0)) { + REQUIRE(vertex_id(g, v) < static_cast(g.size())); + ++count; + } + REQUIRE(count == 2); +} + +TEST_CASE("vertices_bfs - with value function", "[bfs][vertices]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {}, // 1 (leaf) + {} // 2 (leaf) + }; + + auto value_fn = [&g](auto v) { return static_cast(vertex_id(g, v)) * 10; }; + + std::vector values; + for (auto [v, val] : vertices_bfs(g, 0, value_fn)) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + REQUIRE(values[0] == 0); // 0 * 10 + REQUIRE(std::find(values.begin() + 1, values.end(), 10) != values.end()); // 1 * 10 + REQUIRE(std::find(values.begin() + 1, values.end(), 20) != values.end()); // 2 * 10 +} + +TEST_CASE("vertices_bfs - depth tracking", "[bfs][vertices]") { + using Graph = std::vector>; + // Create chain: 0 -> 1 -> 2 -> 3 + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {3}, // 2 -> 3 + {} // 3 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + + for (auto [v] : bfs) { + // Just iterate through + } + + REQUIRE(bfs.depth() == 3); // Path length 0->1->2->3 + REQUIRE(bfs.size() == 3); // 3 vertices visited after seed +} + +TEST_CASE("vertices_bfs - size tracking", "[bfs][vertices]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {}, // 1 (leaf) + {} // 2 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + + int iterations = 0; + for (auto [v] : bfs) { + ++iterations; + } + + REQUIRE(iterations == 3); + REQUIRE(bfs.size() == 2); // 2 vertices visited after seed +} + +TEST_CASE("vertices_bfs - single vertex", "[bfs][vertices]") { + using Graph = std::vector>; + Graph g = { + {} // 0 (leaf) + }; + + std::vector visited; + for (auto [v] : vertices_bfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited.size() == 1); + REQUIRE(visited[0] == 0); +} + +TEST_CASE("vertices_bfs - cycle handling", "[bfs][vertices]") { + using Graph = std::vector>; + // Create cycle: 0 -> 1 -> 2 -> 0 + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {0} // 2 -> 0 (cycle) + }; + + std::vector visited; + for (auto [v] : vertices_bfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Should visit each vertex exactly once + REQUIRE(visited.size() == 3); + REQUIRE(std::count(visited.begin(), visited.end(), 0) == 1); + REQUIRE(std::count(visited.begin(), visited.end(), 1) == 1); + REQUIRE(std::count(visited.begin(), visited.end(), 2) == 1); +} + +TEST_CASE("vertices_bfs - disconnected components", "[bfs][vertices]") { + using Graph = std::vector>; + // Create two disconnected components + Graph g = { + {1}, // 0 -> 1 (Component 1) + {}, // 1 (leaf) + {3}, // 2 -> 3 (Component 2) + {} // 3 (leaf) + }; + + std::vector visited; + for (auto [v] : vertices_bfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Should only visit component containing vertex 0 + REQUIRE(visited.size() == 2); + REQUIRE(std::find(visited.begin(), visited.end(), 0) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 1) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 2) == visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 3) == visited.end()); +} + +TEST_CASE("vertices_bfs - empty iteration", "[bfs][vertices]") { + using Graph = std::vector>; + Graph g = { + {} // 0 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + auto it = bfs.begin(); + auto end = bfs.end(); + + REQUIRE(it != end); // Has seed + ++it; + REQUIRE(it == end); // No more vertices +} + +TEST_CASE("vertices_bfs - cancel_all", "[bfs][vertices][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3}, // 1 -> 3 + {}, // 2 (leaf) + {} // 3 (leaf) + }; + + std::vector visited; + auto bfs = vertices_bfs(g, 0); + + for (auto [v] : bfs) { + visited.push_back(static_cast(vertex_id(g, v))); + if (vertex_id(g, v) == 1) { + bfs.cancel(cancel_search::cancel_all); + } + } + + // Should stop after processing vertex 1 + REQUIRE(visited.size() <= 3); // 0, 1 or 2 (depending on when cancel takes effect) + REQUIRE(std::find(visited.begin(), visited.end(), 3) == visited.end()); // Should not reach 3 +} + +TEST_CASE("vertices_bfs - cancel_branch", "[bfs][vertices][cancel]") { + using Graph = std::vector>; + // Create tree: 0 -> [1, 2], 1 -> [3, 4], 2 -> [5] + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {5}, // 2 -> 5 + {}, // 3 (leaf) + {}, // 4 (leaf) + {} // 5 (leaf) + }; + + std::vector visited; + auto bfs = vertices_bfs(g, 0); + + for (auto [v] : bfs) { + visited.push_back(static_cast(vertex_id(g, v))); + if (vertex_id(g, v) == 1) { + bfs.cancel(cancel_search::cancel_branch); // Skip exploring children of vertex 1 + } + } + + // Should visit: 0, 1, 2, 5 but NOT 3, 4 (children of 1) + REQUIRE(std::find(visited.begin(), visited.end(), 0) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 1) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 2) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 5) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 3) == visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 4) == visited.end()); +} + +TEST_CASE("vertices_bfs - large tree", "[bfs][vertices]") { + using Graph = std::vector>; + // Create binary tree of depth 4 + Graph g(15); + + // Connect as binary tree: node i has children 2i+1 and 2i+2 + for (int i = 0; i < 7; ++i) { + g[static_cast(i)].push_back(2*i+1); + g[static_cast(i)].push_back(2*i+2); + } + + int count = 0; + for (auto [v] : vertices_bfs(g, 0)) { + ++count; + } + + REQUIRE(count == 15); +} + +//============================================================================= +// edges_bfs tests +//============================================================================= + +TEST_CASE("edges_bfs - basic traversal", "[bfs][edges]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {} // 4 (leaf) + }; + + std::vector targets; + for (auto [edge] : edges_bfs(g, 0)) { + auto target_v = target(g, edge); + targets.push_back(static_cast(vertex_id(g, target_v))); + } + + // Should visit edges in BFS order: (0->1), (0->2), (1->3), (1->4) + REQUIRE(targets.size() == 4); + REQUIRE(targets[0] == 1); // First level + REQUIRE(targets[1] == 2); // First level + REQUIRE(targets[2] == 3); // Second level + REQUIRE(targets[3] == 4); // Second level +} + +TEST_CASE("edges_bfs - structured bindings", "[bfs][edges]") { + using Graph = std::vector>; + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {} // 2 (leaf) + }; + + int count = 0; + for (auto [edge] : edges_bfs(g, 0)) { + auto target_v = target(g, edge); + REQUIRE(vertex_id(g, target_v) < g.size()); + ++count; + } + REQUIRE(count == 2); +} + +TEST_CASE("edges_bfs - with value function", "[bfs][edges]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {}, // 1 (leaf) + {} // 2 (leaf) + }; + + auto value_fn = [&g](auto edge) { + auto target_v = target(g, edge); + return static_cast(vertex_id(g, target_v)) * 10; + }; + + std::vector values; + for (auto [edge, val] : edges_bfs(g, 0, value_fn)) { + values.push_back(val); + } + + REQUIRE(values.size() == 2); + REQUIRE(std::find(values.begin(), values.end(), 10) != values.end()); // edge to vertex 1 + REQUIRE(std::find(values.begin(), values.end(), 20) != values.end()); // edge to vertex 2 +} + +TEST_CASE("edges_bfs - single vertex (no edges)", "[bfs][edges]") { + using Graph = std::vector>; + Graph g = { + {} // 0 (no outgoing edges) + }; + + int count = 0; + for (auto [edge] : edges_bfs(g, 0)) { + ++count; + } + + REQUIRE(count == 0); // No edges from isolated vertex +} + +TEST_CASE("edges_bfs - cycle handling", "[bfs][edges]") { + using Graph = std::vector>; + // Create cycle: 0 -> 1 -> 2 -> 0 + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {0} // 2 -> 0 (cycle back, should not visit this edge) + }; + + std::vector targets; + for (auto [edge] : edges_bfs(g, 0)) { + auto target_v = target(g, edge); + targets.push_back(static_cast(vertex_id(g, target_v))); + } + + // Should visit only tree edges: 0->1, 1->2 (not the back edge 2->0) + REQUIRE(targets.size() == 2); + REQUIRE(targets[0] == 1); + REQUIRE(targets[1] == 2); +} + +TEST_CASE("edges_bfs - disconnected components", "[bfs][edges]") { + using Graph = std::vector>; + // Two components: 0-1 and 2-3 + Graph g = { + {1}, // 0 -> 1 (Component 1) + {}, // 1 (leaf) + {3}, // 2 -> 3 (Component 2) + {} // 3 (leaf) + }; + + std::vector targets; + for (auto [edge] : edges_bfs(g, 0)) { + auto target_v = target(g, edge); + targets.push_back(static_cast(vertex_id(g, target_v))); + } + + // Should only visit edges in component containing vertex 0 + REQUIRE(targets.size() == 1); + REQUIRE(targets[0] == 1); +} + +TEST_CASE("edges_bfs - cancel_all", "[bfs][edges][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3}, // 1 -> 3 + {}, // 2 (leaf) + {} // 3 (leaf) + }; + + std::vector targets; + auto bfs = edges_bfs(g, 0); + + for (auto [edge] : bfs) { + auto target_v = target(g, edge); + int target_id = static_cast(vertex_id(g, target_v)); + targets.push_back(target_id); + if (target_id == 1) { + bfs.cancel(cancel_search::cancel_all); + } + } + + // Should stop after visiting edge to vertex 1 + REQUIRE(targets.size() <= 2); // May visit edge to 2 if already in queue + REQUIRE(std::find(targets.begin(), targets.end(), 3) == targets.end()); // Should not reach 3 +} + +TEST_CASE("edges_bfs - cancel_branch", "[bfs][edges][cancel]") { + using Graph = std::vector>; + // Create tree: 0 -> [1, 2], 1 -> [3, 4], 2 -> [5] + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {5}, // 2 -> 5 + {}, // 3 (leaf) + {}, // 4 (leaf) + {} // 5 (leaf) + }; + + std::vector targets; + auto bfs = edges_bfs(g, 0); + + for (auto [edge] : bfs) { + auto target_v = target(g, edge); + int target_id = static_cast(vertex_id(g, target_v)); + targets.push_back(target_id); + if (target_id == 1) { + bfs.cancel(cancel_search::cancel_branch); // Skip exploring children of vertex 1 + } + } + + // Should visit: edges to 1, 2, 5 but NOT 3, 4 (children of 1) + REQUIRE(std::find(targets.begin(), targets.end(), 1) != targets.end()); + REQUIRE(std::find(targets.begin(), targets.end(), 2) != targets.end()); + REQUIRE(std::find(targets.begin(), targets.end(), 5) != targets.end()); + REQUIRE(std::find(targets.begin(), targets.end(), 3) == targets.end()); + REQUIRE(std::find(targets.begin(), targets.end(), 4) == targets.end()); +} + +TEST_CASE("edges_bfs - large tree", "[bfs][edges]") { + using Graph = std::vector>; + // Create binary tree of depth 4 + Graph g(15); + + // Connect as binary tree: node i has children 2i+1 and 2i+2 + for (int i = 0; i < 7; ++i) { + g[static_cast(i)].push_back(2*i+1); + g[static_cast(i)].push_back(2*i+2); + } + + int count = 0; + for (auto [edge] : edges_bfs(g, 0)) { + ++count; + } + + REQUIRE(count == 14); // 15 vertices means 14 edges in tree +} + +//============================================================================= +// BFS depth/size accessor tests +//============================================================================= + +TEST_CASE("vertices_bfs - depth increases by level", "[bfs][vertices][depth]") { + using Graph = std::vector>; + // Create multi-level tree: 0 -> [1, 2], 1 -> [3, 4], 2 -> [5, 6] + Graph g = { + {1, 2}, // 0 (level 0) + {3, 4}, // 1 (level 1) + {5, 6}, // 2 (level 1) + {}, // 3 (level 2) + {}, // 4 (level 2) + {}, // 5 (level 2) + {} // 6 (level 2) + }; + + auto bfs = vertices_bfs(g, 0); + std::size_t prev_depth = 0; + int vertex_count = 0; + + for (auto [v] : bfs) { + auto vid = static_cast(vertex_id(g, v)); + ++vertex_count; + + // After processing seed, depth should be at least as high as before + auto current_depth = bfs.depth(); + REQUIRE(current_depth >= prev_depth); + + // Depth should match expected level + if (vid == 0) REQUIRE(current_depth == 0); // Seed at level 0 + else if (vid <= 2) REQUIRE(current_depth >= 1); // Level 1 + else REQUIRE(current_depth >= 2); // Level 2 + + prev_depth = current_depth; + } + + REQUIRE(bfs.depth() == 2); // Final depth is 2 + REQUIRE(vertex_count == 7); // All 7 vertices visited +} + +TEST_CASE("vertices_bfs - size accumulates correctly", "[bfs][vertices][size]") { + using Graph = std::vector>; + // Tree structure + Graph g = { + {1, 2, 3}, // 0 -> 1, 2, 3 + {4}, // 1 -> 4 + {5}, // 2 -> 5 + {}, // 3 (leaf) + {}, // 4 (leaf) + {} // 5 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + std::size_t prev_size = 0; + int vertex_count = 0; + + for (auto [v] : bfs) { + ++vertex_count; + auto current_size = bfs.size(); + + // Size should never decrease (monotonically increasing) + REQUIRE(current_size >= prev_size); + + prev_size = current_size; + } + + // After iteration completes, size should be 5 (all non-seed vertices discovered) + REQUIRE(bfs.size() == 5); // 5 vertices visited after seed (excludes seed itself) + REQUIRE(vertex_count == 6); // Total vertices iterated including seed +} + +TEST_CASE("vertices_bfs - depth on wide tree", "[bfs][vertices][depth]") { + using Graph = std::vector>; + // Wide tree: root has many children, all at same depth + Graph g = { + {1, 2, 3, 4, 5}, // 0 -> 1, 2, 3, 4, 5 (all at depth 1) + {}, // 1 (leaf) + {}, // 2 (leaf) + {}, // 3 (leaf) + {}, // 4 (leaf) + {} // 5 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + + for (auto [v] : bfs) { + // Just iterate + } + + REQUIRE(bfs.depth() == 1); // Max depth is 1 (only two levels) + REQUIRE(bfs.size() == 5); // 5 children visited +} + +TEST_CASE("vertices_bfs - depth on deep chain", "[bfs][vertices][depth]") { + using Graph = std::vector>; + // Long chain: 0 -> 1 -> 2 -> 3 -> 4 -> 5 + Graph g = { + {1}, // 0 + {2}, // 1 + {3}, // 2 + {4}, // 3 + {5}, // 4 + {} // 5 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + + for (auto [v] : bfs) { + // Just iterate + } + + REQUIRE(bfs.depth() == 5); // Max depth is 5 (6 levels: 0 through 5) + REQUIRE(bfs.size() == 5); // 5 vertices after seed +} + +TEST_CASE("vertices_bfs - size on disconnected graph", "[bfs][vertices][size]") { + using Graph = std::vector>; + // Two disconnected components + Graph g = { + {1, 2}, // 0 -> 1, 2 (Component 1) + {}, // 1 (leaf) + {}, // 2 (leaf) + {4}, // 3 -> 4 (Component 2, unreachable from 0) + {} // 4 (leaf) + }; + + auto bfs = vertices_bfs(g, 0); + + for (auto [v] : bfs) { + // Just iterate + } + + REQUIRE(bfs.depth() == 1); // Depth 1 within component + REQUIRE(bfs.size() == 2); // Only 2 vertices reachable from 0 (excludes seed) +} + +TEST_CASE("edges_bfs - depth tracks edge depth", "[bfs][edges][depth]") { + using Graph = std::vector>; + // Tree: 0 -> [1, 2], 1 -> [3, 4] + Graph g = { + {1, 2}, // 0 + {3, 4}, // 1 + {}, // 2 + {}, // 3 + {} // 4 + }; + + auto bfs = edges_bfs(g, 0); + + for (auto [edge] : bfs) { + // Just iterate + } + + REQUIRE(bfs.depth() == 2); // Deepest edge reaches depth 2 + REQUIRE(bfs.size() == 4); // 4 edges total +} + +TEST_CASE("edges_bfs - size counts edges", "[bfs][edges][size]") { + using Graph = std::vector>; + // Binary tree + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {5, 6}, // 2 -> 5, 6 + {}, // 3 + {}, // 4 + {}, // 5 + {} // 6 + }; + + auto bfs = edges_bfs(g, 0); + int edge_count = 0; + + for (auto [edge] : bfs) { + ++edge_count; + // Size should equal edges discovered so far + REQUIRE(bfs.size() == static_cast(edge_count)); + } + + REQUIRE(bfs.size() == 6); // 6 edges total + REQUIRE(edge_count == 6); + REQUIRE(bfs.depth() == 2); // Max depth is 2 +} + +TEST_CASE("vertices_bfs - depth/size with value function", "[bfs][vertices][depth]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 + {3}, // 1 + {}, // 2 + {} // 3 + }; + + auto value_fn = [&g](auto v) { return static_cast(vertex_id(g, v)) * 10; }; + auto bfs = vertices_bfs(g, 0, value_fn); + + for (auto [v, val] : bfs) { + // Just iterate + } + + REQUIRE(bfs.depth() == 2); // Depth is 2 (0 -> 1 -> 3) + REQUIRE(bfs.size() == 3); // 3 vertices after seed +} + +TEST_CASE("edges_bfs - depth/size with value function", "[bfs][edges][depth]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 + {3}, // 1 + {}, // 2 + {} // 3 + }; + + auto value_fn = [&g](auto e) { + return static_cast(vertex_id(g, target(g, e))) * 10; + }; + auto bfs = edges_bfs(g, 0, value_fn); + + for (auto [e, val] : bfs) { + // Just iterate + } + + REQUIRE(bfs.depth() == 2); // Depth is 2 + REQUIRE(bfs.size() == 3); // 3 edges +} diff --git a/tests/views/test_dfs.cpp b/tests/views/test_dfs.cpp new file mode 100644 index 0000000..fa13fc0 --- /dev/null +++ b/tests/views/test_dfs.cpp @@ -0,0 +1,1315 @@ +/** + * @file test_dfs.cpp + * @brief Comprehensive tests for DFS views + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +// ============================================================================= +// Test 1: Basic DFS Traversal Order +// ============================================================================= + +TEST_CASE("vertices_dfs - basic traversal order", "[dfs][vertices]") { + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {} // 4 (leaf) + }; + + SECTION("DFS from vertex 0") { + std::vector visited_order; + + for (auto [v] : vertices_dfs(g, 0)) { + visited_order.push_back(static_cast(vertex_id(g, v))); + } + + // DFS should visit in depth-first order + // Starting at 0, go to 1, then 3, backtrack to 1, go to 4, backtrack to 0, go to 2 + REQUIRE(visited_order.size() == 5); + REQUIRE(visited_order[0] == 0); // Start at seed + + // All vertices visited + std::set visited_set(visited_order.begin(), visited_order.end()); + REQUIRE(visited_set == std::set{0, 1, 2, 3, 4}); + } + + SECTION("DFS from leaf vertex") { + std::vector visited_order; + + for (auto [v] : vertices_dfs(g, 3)) { + visited_order.push_back(static_cast(vertex_id(g, v))); + } + + // Only vertex 3 should be visited (no outgoing edges) + REQUIRE(visited_order == std::vector{3}); + } +} + +// ============================================================================= +// Test 2: Structured Bindings +// ============================================================================= + +TEST_CASE("vertices_dfs - structured bindings", "[dfs][vertices][bindings]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + SECTION("structured binding [v]") { + std::vector ids; + + for (auto [v] : vertices_dfs(g, 0)) { + ids.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(ids.size() == 3); + } + + SECTION("structured binding [v, val] with value function") { + auto dfs = vertices_dfs(g, 0, [&g](auto v) { + return vertex_id(g, v) * 10; + }); + + std::vector> results; + for (auto [v, val] : dfs) { + results.emplace_back(static_cast(vertex_id(g, v)), val); + } + + REQUIRE(results.size() == 3); + for (auto& [id, val] : results) { + REQUIRE(val == id * 10); + } + } +} + +// ============================================================================= +// Test 3: Visited Tracking (No Revisits) +// ============================================================================= + +TEST_CASE("vertices_dfs - visited tracking", "[dfs][vertices][visited]") { + // Graph with cycle: 0 -> 1 -> 2 -> 0 + using Graph = std::vector>; + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {0} // 2 -> 0 (back edge) + }; + + std::vector visited_order; + + for (auto [v] : vertices_dfs(g, 0)) { + visited_order.push_back(static_cast(vertex_id(g, v))); + } + + // Each vertex visited exactly once despite cycle + REQUIRE(visited_order.size() == 3); + std::set visited_set(visited_order.begin(), visited_order.end()); + REQUIRE(visited_set == std::set{0, 1, 2}); +} + +// ============================================================================= +// Test 4: Value Function Types +// ============================================================================= + +TEST_CASE("vertices_dfs - value function types", "[dfs][vertices][vvf]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {}, + {} + }; + + SECTION("returning int") { + auto dfs = vertices_dfs(g, 0, [&g](auto v) { + return static_cast(vertex_id(g, v)); + }); + + int sum = 0; + for (auto [v, val] : dfs) { + sum += val; + } + REQUIRE(sum == 0 + 1 + 2); + } + + SECTION("returning string") { + auto dfs = vertices_dfs(g, 0, [&g](auto v) { + return "v" + std::to_string(vertex_id(g, v)); + }); + + std::vector names; + for (auto [v, name] : dfs) { + names.push_back(name); + } + + REQUIRE(names.size() == 3); + REQUIRE(std::find(names.begin(), names.end(), "v0") != names.end()); + REQUIRE(std::find(names.begin(), names.end(), "v1") != names.end()); + REQUIRE(std::find(names.begin(), names.end(), "v2") != names.end()); + } + + SECTION("capturing lambda") { + int multiplier = 5; + auto dfs = vertices_dfs(g, 0, [&g, multiplier](auto v) { + return static_cast(vertex_id(g, v)) * multiplier; + }); + + std::vector values; + for (auto [v, val] : dfs) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + // Values are id * 5 + std::set value_set(values.begin(), values.end()); + REQUIRE(value_set == std::set{0, 5, 10}); + } +} + +// ============================================================================= +// Test 5: Depth and Size Accessors +// ============================================================================= + +TEST_CASE("vertices_dfs - depth and size accessors", "[dfs][vertices][accessors]") { + // 0 + // /|\ + // 1 2 3 + // | + // 4 + // | + // 5 + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // 0 -> 1, 2, 3 + {4}, // 1 -> 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {5}, // 4 -> 5 + {} // 5 (leaf) + }; + + auto dfs = vertices_dfs(g, 0); + + // Before iteration + REQUIRE(dfs.depth() == 1); // seed is on stack + REQUIRE(dfs.size() == 0); // no vertices counted yet + + // Iterate + std::vector visited; + for (auto [v] : dfs) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // After full iteration + REQUIRE(visited.size() == 6); + REQUIRE(dfs.size() == 5); // All vertices except seed are counted by advance() +} + +// ============================================================================= +// Test 6: Graph Topologies +// ============================================================================= + +TEST_CASE("vertices_dfs - tree topology", "[dfs][vertices][topology]") { + // Binary tree + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3, 4}, + {}, + {}, + {} + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited.size() == 5); + REQUIRE(visited[0] == 0); // Start + // DFS goes deep first: 0 -> 1 -> 3, backtrack to 1 -> 4, backtrack to 0 -> 2 +} + +TEST_CASE("vertices_dfs - cycle topology", "[dfs][vertices][topology]") { + // Ring: 0 -> 1 -> 2 -> 3 -> 0 + using Graph = std::vector>; + Graph g = { + {1}, + {2}, + {3}, + {0} + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Should visit all 4 vertices exactly once + REQUIRE(visited.size() == 4); + std::set visited_set(visited.begin(), visited.end()); + REQUIRE(visited_set == std::set{0, 1, 2, 3}); +} + +TEST_CASE("vertices_dfs - DAG topology", "[dfs][vertices][topology]") { + // Diamond DAG + // 0 + // / \ + // 1 2 + // \ / + // 3 + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3}, + {3}, + {} + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Vertex 3 visited only once even though reachable from both 1 and 2 + REQUIRE(visited.size() == 4); + REQUIRE(std::count(visited.begin(), visited.end(), 3) == 1); +} + +TEST_CASE("vertices_dfs - disconnected graph", "[dfs][vertices][topology]") { + // Two components: 0-1-2 and 3-4 + using Graph = std::vector>; + Graph g = { + {1}, + {2}, + {}, + {4}, + {} + }; + + SECTION("DFS from component 1") { + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Only vertices 0, 1, 2 reachable from 0 + REQUIRE(visited.size() == 3); + std::set visited_set(visited.begin(), visited.end()); + REQUIRE(visited_set == std::set{0, 1, 2}); + } + + SECTION("DFS from component 2") { + std::vector visited; + for (auto [v] : vertices_dfs(g, 3)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Only vertices 3, 4 reachable from 3 + REQUIRE(visited.size() == 2); + std::set visited_set(visited.begin(), visited.end()); + REQUIRE(visited_set == std::set{3, 4}); + } +} + +// ============================================================================= +// Test 7: Empty Graph and Single Vertex +// ============================================================================= + +TEST_CASE("vertices_dfs - single vertex graph", "[dfs][vertices][edge_cases]") { + using Graph = std::vector>; + Graph g = { + {} // Single vertex with no edges + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited == std::vector{0}); +} + +TEST_CASE("vertices_dfs - vertex with no outgoing edges", "[dfs][vertices][edge_cases]") { + using Graph = std::vector>; + Graph g = { + {1}, + {}, + {0} // Has incoming but no reachable from it + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 1)) { // Start from leaf + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited == std::vector{1}); +} + +// ============================================================================= +// Test 8: search_view Concept +// ============================================================================= + +TEST_CASE("vertices_dfs - search_view concept", "[dfs][vertices][concepts]") { + using Graph = std::vector>; + Graph g = {{1}, {}}; + + auto dfs = vertices_dfs(g, 0); + + STATIC_REQUIRE(search_view); + + // Verify accessors exist and return correct types + REQUIRE(dfs.cancel() == cancel_search::continue_search); + REQUIRE(dfs.depth() >= 0); + REQUIRE(dfs.size() >= 0); +} + +// ============================================================================= +// Test 9: Range Concepts +// ============================================================================= + +TEST_CASE("vertices_dfs - range concepts", "[dfs][vertices][concepts]") { + using Graph = std::vector>; + Graph g = {{1}, {}}; + + auto dfs = vertices_dfs(g, 0); + + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::view); + + // DFS is input range (not forward) due to shared state + // Cannot iterate twice independently +} + +// ============================================================================= +// Test 10: vertex_info Type Verification +// ============================================================================= + +TEST_CASE("vertices_dfs - vertex_info type verification", "[dfs][vertices][types]") { + using Graph = std::vector>; + using VertexType = vertex_t; + + SECTION("no value function") { + using ViewType = vertices_dfs_view>; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + } + + SECTION("with value function") { + using VVF = int(*)(VertexType); + using ViewType = vertices_dfs_view>; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + } +} + +// ============================================================================= +// Test 11: Deque-based Graph +// ============================================================================= + +TEST_CASE("vertices_dfs - deque-based graph", "[dfs][vertices][deque]") { + using Graph = std::deque>; + Graph g = { + {1, 2}, + {}, + {} + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited.size() == 3); +} + +// ============================================================================= +// Test 12: Weighted Graph +// ============================================================================= + +TEST_CASE("vertices_dfs - weighted graph", "[dfs][vertices][weighted]") { + using Graph = std::vector>>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, + {{2, 3.5}}, + {} + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited.size() == 3); + std::set visited_set(visited.begin(), visited.end()); + REQUIRE(visited_set == std::set{0, 1, 2}); +} + +// ============================================================================= +// Test 13: Large Graph Performance +// ============================================================================= + +TEST_CASE("vertices_dfs - large linear graph", "[dfs][vertices][performance]") { + // Linear chain: 0 -> 1 -> 2 -> ... -> 999 + using Graph = std::vector>; + Graph g(1000); + for (int i = 0; i < 999; ++i) { + g[i].push_back(i + 1); + } + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(visited.size() == 1000); + + // Verify order: should be 0, 1, 2, ..., 999 + for (int i = 0; i < 1000; ++i) { + REQUIRE(visited[i] == i); + } +} + +// ============================================================================= +// Test 14: DFS Pre-order Property +// ============================================================================= + +TEST_CASE("vertices_dfs - pre-order property", "[dfs][vertices][order]") { + // Verify parent is visited before children + // 0 + // / \ + // 1 2 + // /| |\ + // 3 4 5 6 + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 + {3, 4}, // 1 + {5, 6}, // 2 + {}, // 3 + {}, // 4 + {}, // 5 + {} // 6 + }; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // Find positions + auto pos = [&visited](int id) { + return std::find(visited.begin(), visited.end(), id) - visited.begin(); + }; + + // Parent visited before children + REQUIRE(pos(0) < pos(1)); + REQUIRE(pos(0) < pos(2)); + REQUIRE(pos(1) < pos(3)); + REQUIRE(pos(1) < pos(4)); + REQUIRE(pos(2) < pos(5)); + REQUIRE(pos(2) < pos(6)); +} + +// ============================================================================= +// edges_dfs Tests +// ============================================================================= + +// ============================================================================= +// Test 15: edges_dfs Basic Traversal +// ============================================================================= + +TEST_CASE("edges_dfs - basic traversal order", "[dfs][edges]") { + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {} // 4 (leaf) + }; + + SECTION("edges from vertex 0") { + std::vector> edges_visited; + + for (auto [e] : edges_dfs(g, 0)) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + } + + // DFS tree edges: 0->1, 1->3, 1->4, 0->2 + // Note: Seed vertex 0 has no incoming edge, so we get 4 tree edges + REQUIRE(edges_visited.size() == 4); + + // All edges are tree edges to unvisited vertices + std::set targets; + for (auto [src, tgt] : edges_visited) { + targets.insert(tgt); + } + REQUIRE(targets == std::set{1, 2, 3, 4}); + } + + SECTION("edges from leaf vertex") { + std::vector> edges_visited; + + for (auto [e] : edges_dfs(g, 3)) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + } + + // Leaf vertex has no outgoing edges + REQUIRE(edges_visited.empty()); + } +} + +// ============================================================================= +// Test 16: edges_dfs Structured Bindings +// ============================================================================= + +TEST_CASE("edges_dfs - structured bindings", "[dfs][edges][bindings]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + SECTION("structured binding [e]") { + int count = 0; + + for (auto [e] : edges_dfs(g, 0)) { + // Access edge through structured binding + [[maybe_unused]] auto tgt = target_id(g, e); + count++; + } + + // Two edges: 0->1, 1->2 (note: 0->2 is skipped because 2 already visited via 1) + REQUIRE(count == 2); + } + + SECTION("structured binding [e, val] with value function") { + auto dfs = edges_dfs(g, 0, [&g](auto e) { + return target_id(g, e) * 10; + }); + + std::vector> results; + for (auto [e, val] : dfs) { + results.emplace_back(static_cast(target_id(g, e)), val); + } + + REQUIRE(results.size() == 2); + for (auto& [tgt, val] : results) { + REQUIRE(val == tgt * 10); + } + } +} + +// ============================================================================= +// Test 17: edges_dfs Value Function Types +// ============================================================================= + +TEST_CASE("edges_dfs - value function types", "[dfs][edges][evf]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {}, + {} + }; + + SECTION("returning int") { + auto dfs = edges_dfs(g, 0, [&g](auto e) { + return static_cast(target_id(g, e)); + }); + + int sum = 0; + for (auto [e, val] : dfs) { + sum += val; + } + REQUIRE(sum == 1 + 2); + } + + SECTION("returning string") { + auto dfs = edges_dfs(g, 0, [&g](auto e) { + return "e" + std::to_string(source_id(g, e)) + "_" + std::to_string(target_id(g, e)); + }); + + std::vector names; + for (auto [e, name] : dfs) { + names.push_back(name); + } + + REQUIRE(names.size() == 2); + // Order depends on DFS, but should have both edges + std::set name_set(names.begin(), names.end()); + REQUIRE(name_set == std::set{"e0_1", "e0_2"}); + } + + SECTION("capturing lambda") { + int multiplier = 5; + auto dfs = edges_dfs(g, 0, [&g, multiplier](auto e) { + return static_cast(target_id(g, e)) * multiplier; + }); + + std::vector values; + for (auto [e, val] : dfs) { + values.push_back(val); + } + + REQUIRE(values.size() == 2); + std::set value_set(values.begin(), values.end()); + REQUIRE(value_set == std::set{5, 10}); // 1*5, 2*5 + } +} + +// ============================================================================= +// Test 18: edges_dfs Cycle Handling +// ============================================================================= + +TEST_CASE("edges_dfs - cycle handling", "[dfs][edges][visited]") { + // Graph with cycle: 0 -> 1 -> 2 -> 0 + using Graph = std::vector>; + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {0} // 2 -> 0 (back edge) + }; + + std::vector> edges_visited; + + for (auto [e] : edges_dfs(g, 0)) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + } + + // Only tree edges: 0->1, 1->2 + // The back edge 2->0 is NOT yielded because vertex 0 is already visited + REQUIRE(edges_visited.size() == 2); + REQUIRE(edges_visited[0] == std::pair{0, 1}); + REQUIRE(edges_visited[1] == std::pair{1, 2}); +} + +// ============================================================================= +// Test 19: edges_dfs Diamond DAG +// ============================================================================= + +TEST_CASE("edges_dfs - diamond DAG", "[dfs][edges][topology]") { + // Diamond DAG + // 0 + // / \ + // 1 2 + // \ / + // 3 + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3}, + {3}, + {} + }; + + std::vector> edges_visited; + for (auto [e] : edges_dfs(g, 0)) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + } + + // Tree edges: 0->1, 1->3, 0->2 (2->3 skipped because 3 already visited) + REQUIRE(edges_visited.size() == 3); + + // Verify all tree edges reach unique targets + std::set targets; + for (auto [src, tgt] : edges_visited) { + targets.insert(tgt); + } + REQUIRE(targets == std::set{1, 2, 3}); +} + +// ============================================================================= +// Test 20: edges_dfs Disconnected Graph +// ============================================================================= + +TEST_CASE("edges_dfs - disconnected graph", "[dfs][edges][topology]") { + // Two components: 0-1-2 and 3-4 + using Graph = std::vector>; + Graph g = { + {1}, + {2}, + {}, + {4}, + {} + }; + + SECTION("edges from component 1") { + std::vector> edges_visited; + for (auto [e] : edges_dfs(g, 0)) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + } + + // Only edges in component 1 + REQUIRE(edges_visited.size() == 2); + REQUIRE(edges_visited[0] == std::pair{0, 1}); + REQUIRE(edges_visited[1] == std::pair{1, 2}); + } + + SECTION("edges from component 2") { + std::vector> edges_visited; + for (auto [e] : edges_dfs(g, 3)) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + } + + // Only edge in component 2 + REQUIRE(edges_visited.size() == 1); + REQUIRE(edges_visited[0] == std::pair{3, 4}); + } +} + +// ============================================================================= +// Test 21: edges_dfs Single Vertex +// ============================================================================= + +TEST_CASE("edges_dfs - single vertex graph", "[dfs][edges][edge_cases]") { + using Graph = std::vector>; + Graph g = { + {} // Single vertex with no edges + }; + + std::vector edge_count; + for (auto [e] : edges_dfs(g, 0)) { + edge_count.push_back(1); + } + + REQUIRE(edge_count.empty()); +} + +// ============================================================================= +// Test 22: edges_dfs search_view Concept +// ============================================================================= + +TEST_CASE("edges_dfs - search_view concept", "[dfs][edges][concepts]") { + using Graph = std::vector>; + Graph g = {{1}, {}}; + + auto dfs = edges_dfs(g, 0); + + STATIC_REQUIRE(search_view); + + // Verify accessors exist and return correct types + REQUIRE(dfs.cancel() == cancel_search::continue_search); + REQUIRE(dfs.depth() >= 0); + REQUIRE(dfs.size() >= 0); +} + +// ============================================================================= +// Test 23: edges_dfs Range Concepts +// ============================================================================= + +TEST_CASE("edges_dfs - range concepts", "[dfs][edges][concepts]") { + using Graph = std::vector>; + Graph g = {{1}, {}}; + + auto dfs = edges_dfs(g, 0); + + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::view); +} + +// ============================================================================= +// Test 24: edges_dfs edge_info Type Verification +// ============================================================================= + +TEST_CASE("edges_dfs - edge_info type verification", "[dfs][edges][types]") { + using Graph = std::vector>; + using EdgeType = edge_t; + + SECTION("no value function") { + using ViewType = edges_dfs_view>; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + } + + SECTION("with value function") { + using EVF = int(*)(EdgeType); + using ViewType = edges_dfs_view>; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + } +} + +// ============================================================================= +// Test 25: edges_dfs Weighted Graph +// ============================================================================= + +TEST_CASE("edges_dfs - weighted graph", "[dfs][edges][weighted]") { + using Graph = std::vector>>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, + {{2, 3.5}}, + {} + }; + + std::vector weights; + for (auto [e] : edges_dfs(g, 0)) { + // Edge value is the weight in the pair + weights.push_back(edge_value(g, e)); + } + + // Two tree edges: 0->1 (1.5), 1->2 (3.5) + // Note: 0->2 is skipped because 2 is already visited via 1 + REQUIRE(weights.size() == 2); + REQUIRE(weights[0] == 1.5); + REQUIRE(weights[1] == 3.5); +} + +// ============================================================================= +// Test 26: edges_dfs Large Graph +// ============================================================================= + +TEST_CASE("edges_dfs - large linear graph", "[dfs][edges][performance]") { + // Linear chain: 0 -> 1 -> 2 -> ... -> 999 + using Graph = std::vector>; + Graph g(1000); + for (int i = 0; i < 999; ++i) { + g[i].push_back(i + 1); + } + + int edge_count = 0; + for (auto [e] : edges_dfs(g, 0)) { + ++edge_count; + } + + // 999 tree edges + REQUIRE(edge_count == 999); +} + +// ============================================================================= +// Test 27: edges_dfs Deque-based Graph +// ============================================================================= + +TEST_CASE("edges_dfs - deque-based graph", "[dfs][edges][deque]") { + using Graph = std::deque>; + Graph g = { + {1, 2}, + {}, + {} + }; + + std::vector targets; + for (auto [e] : edges_dfs(g, 0)) { + targets.push_back(static_cast(target_id(g, e))); + } + + REQUIRE(targets.size() == 2); + std::set target_set(targets.begin(), targets.end()); + REQUIRE(target_set == std::set{1, 2}); +} + +// ============================================================================= +// Test 28: edges_dfs Depth and Size Accessors +// ============================================================================= + +TEST_CASE("edges_dfs - depth and size accessors", "[dfs][edges][accessors]") { + // 0 + // /|\ + // 1 2 3 + // | + // 4 + // | + // 5 + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // 0 -> 1, 2, 3 + {4}, // 1 -> 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {5}, // 4 -> 5 + {} // 5 (leaf) + }; + + auto dfs = edges_dfs(g, 0); + + // Before iteration + REQUIRE(dfs.depth() == 1); // seed is on stack + REQUIRE(dfs.size() == 0); // no edges counted yet + + // Iterate + int edge_count = 0; + for ([[maybe_unused]] auto [e] : dfs) { + edge_count++; + } + + // 5 tree edges (one for each non-seed vertex) + REQUIRE(edge_count == 5); + REQUIRE(dfs.size() == 5); +} + +// ============================================================================= +// DFS Cancel Functionality Tests (Step 3.3) +// ============================================================================= + +// ============================================================================= +// Test 29: vertices_dfs cancel_all +// ============================================================================= + +TEST_CASE("vertices_dfs - cancel_all stops traversal", "[dfs][vertices][cancel]") { + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {} // 4 (leaf) + }; + + std::vector visited; + auto dfs = vertices_dfs(g, 0); + + for (auto [v] : dfs) { + visited.push_back(static_cast(vertex_id(g, v))); + if (vertex_id(g, v) == 1) { + dfs.cancel(cancel_search::cancel_all); + } + } + + // Should stop immediately after setting cancel_all + // Visited: 0, 1 (cancel_all set after visiting 1) + REQUIRE(visited.size() == 2); + REQUIRE(visited[0] == 0); + REQUIRE(visited[1] == 1); + + // Cancel state should be cancel_all + REQUIRE(dfs.cancel() == cancel_search::cancel_all); +} + +// ============================================================================= +// Test 30: vertices_dfs cancel_branch skips subtree +// ============================================================================= + +TEST_CASE("vertices_dfs - cancel_branch skips subtree", "[dfs][vertices][cancel]") { + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + using Graph = std::vector>; + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3, 4}, // 1 -> 3, 4 + {}, // 2 (leaf) + {}, // 3 (leaf) + {} // 4 (leaf) + }; + + std::vector visited; + auto dfs = vertices_dfs(g, 0); + + for (auto [v] : dfs) { + visited.push_back(static_cast(vertex_id(g, v))); + if (vertex_id(g, v) == 1) { + // Skip vertex 1's subtree (3, 4) + dfs.cancel(cancel_search::cancel_branch); + } + } + + // Should visit 0, 1, then skip 3 and 4 (subtree of 1), then visit 2 + // Visited: 0, 1, 2 + REQUIRE(visited.size() == 3); + std::set visited_set(visited.begin(), visited.end()); + REQUIRE(visited_set == std::set{0, 1, 2}); + + // 3 and 4 should NOT be visited + REQUIRE(std::find(visited.begin(), visited.end(), 3) == visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 4) == visited.end()); + + // Cancel state should be reset to continue_search + REQUIRE(dfs.cancel() == cancel_search::continue_search); +} + +// ============================================================================= +// Test 31: vertices_dfs continue_search normal behavior +// ============================================================================= + +TEST_CASE("vertices_dfs - continue_search normal behavior", "[dfs][vertices][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {}, + {} + }; + + auto dfs = vertices_dfs(g, 0); + + // Default is continue_search + REQUIRE(dfs.cancel() == cancel_search::continue_search); + + std::vector visited; + for (auto [v] : dfs) { + visited.push_back(static_cast(vertex_id(g, v))); + } + + // All vertices visited + REQUIRE(visited.size() == 3); +} + +// ============================================================================= +// Test 32: vertices_dfs cancel state propagates through iterator copies +// ============================================================================= + +TEST_CASE("vertices_dfs - cancel state propagates through shared state", "[dfs][vertices][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3}, + {} + }; + + auto dfs = vertices_dfs(g, 0); + auto it = dfs.begin(); + + // Advance a bit + ++it; // now at vertex 1 + + // Cancel via view + dfs.cancel(cancel_search::cancel_all); + + // Iterator should also see the cancel (shared state) + ++it; // should stop + + REQUIRE(it.at_end()); +} + +// ============================================================================= +// Test 33: edges_dfs cancel_all stops traversal +// ============================================================================= + +TEST_CASE("edges_dfs - cancel_all stops traversal", "[dfs][edges][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3, 4}, + {}, + {}, + {} + }; + + std::vector> edges_visited; + auto dfs = edges_dfs(g, 0); + + for (auto [e] : dfs) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + if (tgt == 3) { + dfs.cancel(cancel_search::cancel_all); + } + } + + // Should stop after edge to 3 + // Edges: 0->1, 1->3 (cancel_all set after this) + REQUIRE(edges_visited.size() == 2); + REQUIRE(dfs.cancel() == cancel_search::cancel_all); +} + +// ============================================================================= +// Test 34: edges_dfs cancel_branch skips subtree +// ============================================================================= + +TEST_CASE("edges_dfs - cancel_branch skips subtree", "[dfs][edges][cancel]") { + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3, 4}, + {}, + {}, + {} + }; + + std::vector> edges_visited; + auto dfs = edges_dfs(g, 0); + + for (auto [e] : dfs) { + auto src = static_cast(source_id(g, e)); + auto tgt = static_cast(target_id(g, e)); + edges_visited.emplace_back(src, tgt); + if (tgt == 1) { + // Skip subtree rooted at 1 (edges 1->3, 1->4) + dfs.cancel(cancel_search::cancel_branch); + } + } + + // Should see edge 0->1 (then skip subtree), then edge 0->2 + // Edges: 0->1, 0->2 + REQUIRE(edges_visited.size() == 2); + + std::set targets; + for (auto [src, tgt] : edges_visited) { + targets.insert(tgt); + } + REQUIRE(targets == std::set{1, 2}); + + // 3 and 4 should NOT be reached + REQUIRE(targets.find(3) == targets.end()); + REQUIRE(targets.find(4) == targets.end()); +} + +// ============================================================================= +// Test 35: cancel_branch at root has no subtree to skip +// ============================================================================= + +TEST_CASE("vertices_dfs - cancel_branch at seed vertex", "[dfs][vertices][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {}, + {} + }; + + std::vector visited; + auto dfs = vertices_dfs(g, 0); + + for (auto [v] : dfs) { + visited.push_back(static_cast(vertex_id(g, v))); + if (vertex_id(g, v) == 0) { + // Cancel at root - should skip entire traversal + dfs.cancel(cancel_search::cancel_branch); + } + } + + // Only seed vertex visited, subtree (1, 2) skipped + REQUIRE(visited.size() == 1); + REQUIRE(visited[0] == 0); +} + +// ============================================================================= +// Test 36: Multiple cancel_branch calls +// ============================================================================= + +TEST_CASE("vertices_dfs - multiple cancel_branch calls", "[dfs][vertices][cancel]") { + // 0 + // / | \ + // 1 2 3 + // /| | |\ + // 4 5 6 7 8 + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // 0 -> 1, 2, 3 + {4, 5}, // 1 -> 4, 5 + {6}, // 2 -> 6 + {7, 8}, // 3 -> 7, 8 + {}, // 4 + {}, // 5 + {}, // 6 + {}, // 7 + {} // 8 + }; + + std::vector visited; + auto dfs = vertices_dfs(g, 0); + + for (auto [v] : dfs) { + int vid = static_cast(vertex_id(g, v)); + visited.push_back(vid); + // Skip subtrees of vertices 1 and 3 + if (vid == 1 || vid == 3) { + dfs.cancel(cancel_search::cancel_branch); + } + } + + // Should visit: 0, 1 (skip 4,5), 2, 6, 3 (skip 7,8) + // Note: exact order depends on DFS exploration + std::set visited_set(visited.begin(), visited.end()); + REQUIRE(visited_set == std::set{0, 1, 2, 3, 6}); + + // Vertices 4, 5, 7, 8 should NOT be visited + REQUIRE(visited_set.find(4) == visited_set.end()); + REQUIRE(visited_set.find(5) == visited_set.end()); + REQUIRE(visited_set.find(7) == visited_set.end()); + REQUIRE(visited_set.find(8) == visited_set.end()); +} + +// ============================================================================= +// Test 37: cancel with value function +// ============================================================================= + +TEST_CASE("vertices_dfs - cancel with value function", "[dfs][vertices][cancel]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3, 4}, + {}, + {}, + {} + }; + + std::vector> results; + auto dfs = vertices_dfs(g, 0, [&g](auto v) { + return static_cast(vertex_id(g, v)) * 10; + }); + + for (auto [v, val] : dfs) { + int vid = static_cast(vertex_id(g, v)); + results.emplace_back(vid, val); + if (vid == 1) { + dfs.cancel(cancel_search::cancel_branch); + } + } + + // Should visit 0, 1 (skip subtree), 2 + REQUIRE(results.size() == 3); + + // Verify value function was called correctly + for (auto& [id, val] : results) { + REQUIRE(val == id * 10); + } +} diff --git a/tests/views/test_edge_cases.cpp b/tests/views/test_edge_cases.cpp new file mode 100644 index 0000000..9cc4981 --- /dev/null +++ b/tests/views/test_edge_cases.cpp @@ -0,0 +1,646 @@ +/** + * @file test_edge_cases.cpp + * @brief Comprehensive edge case tests for graph views + * + * Tests cover: + * - Empty graphs + * - Single vertex graphs + * - Disconnected graphs + * - Self-loops + * - Parallel edges + * - Const graphs + * - Exception safety + */ + +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views::adaptors; + +//============================================================================= +// Empty Graph Tests +//============================================================================= + +TEST_CASE("Empty graph - vertexlist view", "[views][edge_cases][empty]") { + std::vector> g; // Empty graph + + auto view = g | vertexlist(); + + REQUIRE(std::ranges::distance(view) == 0); + REQUIRE(view.begin() == view.end()); + + // Should not iterate at all + size_t count = 0; + for (auto [v] : view) { + (void)v; + ++count; + } + REQUIRE(count == 0); +} + +TEST_CASE("Empty graph - edgelist view", "[views][edge_cases][empty]") { + std::vector> g; // Empty graph + + auto view = g | edgelist(); + + REQUIRE(std::ranges::distance(view) == 0); + REQUIRE(view.begin() == view.end()); +} + +TEST_CASE("Empty graph - DFS vertices", "[views][edge_cases][empty]") { + std::vector> g; // Empty graph + + // Cannot perform DFS on empty graph (no seed vertex) + // This test verifies we handle the boundary case properly + REQUIRE(g.empty()); +} + +//============================================================================= +// Single Vertex Tests +//============================================================================= + +TEST_CASE("Single vertex - no edges", "[views][edge_cases][single_vertex]") { + std::vector> g(1); // One vertex, no edges + + SECTION("vertexlist") { + auto view = g | vertexlist(); + REQUIRE(std::ranges::distance(view) == 1); + + for (auto [v] : view) { + REQUIRE(vertex_id(g, v) == 0); + } + } + + SECTION("incidence from vertex 0") { + auto view = g | incidence(0); + REQUIRE(std::ranges::distance(view) == 0); // No edges + } + + SECTION("neighbors from vertex 0") { + auto view = g | neighbors(0); + REQUIRE(std::ranges::distance(view) == 0); // No neighbors + } + + SECTION("edgelist") { + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 0); // No edges + } +} + +TEST_CASE("Single vertex - self-loop", "[views][edge_cases][single_vertex][self_loop]") { + std::vector> g(1); + g[0].push_back(0); // Self-loop + + SECTION("incidence") { + auto view = g | incidence(0); + REQUIRE(std::ranges::distance(view) == 1); + + for (auto [e] : view) { + REQUIRE(source_id(g, e) == 0); + REQUIRE(target_id(g, e) == 0); + } + } + + SECTION("neighbors") { + auto view = g | neighbors(0); + REQUIRE(std::ranges::distance(view) == 1); + + for (auto [v] : view) { + REQUIRE(vertex_id(g, v) == 0); // Points to itself + } + } + + SECTION("edgelist") { + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 1); + + for (auto [e] : view) { + REQUIRE(source_id(g, e) == 0); + REQUIRE(target_id(g, e) == 0); + } + } +} + +//============================================================================= +// Disconnected Graph Tests +//============================================================================= + +TEST_CASE("Disconnected graph - DFS reaches only one component", "[views][edge_cases][disconnected]") { + std::vector> g(6); + + // Component 1: 0 -> 1 -> 2 + g[0].push_back(1); + g[1].push_back(2); + + // Component 2: 3 -> 4 -> 5 + g[3].push_back(4); + g[4].push_back(5); + + SECTION("DFS from component 1") { + auto view = g | vertices_dfs(0); + std::vector visited; + + for (auto [v] : view) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 3); + // Should visit only vertices 0, 1, 2 + REQUIRE(std::find(visited.begin(), visited.end(), 0) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 1) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 2) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 3) == visited.end()); + } + + SECTION("DFS from component 2") { + auto view = g | vertices_dfs(3); + std::vector visited; + + for (auto [v] : view) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 3); + // Should visit only vertices 3, 4, 5 + REQUIRE(std::find(visited.begin(), visited.end(), 3) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 4) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 5) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 0) == visited.end()); + } +} + +TEST_CASE("Disconnected graph - BFS reaches only one component", "[views][edge_cases][disconnected]") { + std::vector> g(6); + + // Component 1: 0 -> 1, 0 -> 2 + g[0].push_back(1); + g[0].push_back(2); + + // Component 2: 3 -> 4, 3 -> 5 + g[3].push_back(4); + g[3].push_back(5); + + auto view = g | vertices_bfs(0); + std::vector visited; + + for (auto [v] : view) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 3); + REQUIRE(std::find(visited.begin(), visited.end(), 0) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 1) != visited.end()); + REQUIRE(std::find(visited.begin(), visited.end(), 2) != visited.end()); +} + +TEST_CASE("Disconnected graph - topological sort includes all components", "[views][edge_cases][disconnected]") { + std::vector> g(6); + + // Component 1: 0 -> 1 -> 2 + g[0].push_back(1); + g[1].push_back(2); + + // Component 2: 3 -> 4 -> 5 + g[3].push_back(4); + g[4].push_back(5); + + auto view = g | vertices_topological_sort(); + std::vector order; + + for (auto [v] : view) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 6); // All vertices included + + // Verify topological property for component 1 + auto pos0 = std::find(order.begin(), order.end(), 0); + auto pos1 = std::find(order.begin(), order.end(), 1); + auto pos2 = std::find(order.begin(), order.end(), 2); + REQUIRE(pos0 < pos1); + REQUIRE(pos1 < pos2); + + // Verify topological property for component 2 + auto pos3 = std::find(order.begin(), order.end(), 3); + auto pos4 = std::find(order.begin(), order.end(), 4); + auto pos5 = std::find(order.begin(), order.end(), 5); + REQUIRE(pos3 < pos4); + REQUIRE(pos4 < pos5); +} + +//============================================================================= +// Self-Loop Tests +//============================================================================= + +TEST_CASE("Self-loops - multiple vertices with self-loops", "[views][edge_cases][self_loop]") { + std::vector> g(3); + + g[0].push_back(0); // Self-loop at 0 + g[1].push_back(1); // Self-loop at 1 + g[2].push_back(2); // Self-loop at 2 + + SECTION("edgelist counts all self-loops") { + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 3); + + for (auto [e] : view) { + auto sid = source_id(g, e); + auto tid = target_id(g, e); + REQUIRE(sid == tid); // All are self-loops + } + } + + SECTION("incidence at each vertex") { + for (size_t u = 0; u < 3; ++u) { + auto view = g | incidence(u); + REQUIRE(std::ranges::distance(view) == 1); + + for (auto [e] : view) { + REQUIRE(source_id(g, e) == u); + REQUIRE(target_id(g, e) == u); + } + } + } +} + +//============================================================================= +// Parallel Edges Tests +//============================================================================= + +TEST_CASE("Parallel edges - multiple edges between same vertices", "[views][edge_cases][parallel_edges]") { + std::vector> g(3); + + // Multiple edges from 0 to 1 + g[0].push_back(1); + g[0].push_back(1); + g[0].push_back(1); + + // Multiple edges from 1 to 2 + g[1].push_back(2); + g[1].push_back(2); + + SECTION("incidence counts all parallel edges") { + auto view = g | incidence(0); + REQUIRE(std::ranges::distance(view) == 3); // Three parallel edges + + for (auto [e] : view) { + REQUIRE(source_id(g, e) == 0); + REQUIRE(target_id(g, e) == 1); + } + } + + SECTION("neighbors lists parallel edges separately") { + auto view = g | neighbors(0); + REQUIRE(std::ranges::distance(view) == 3); // Three parallel edges + + for (auto [v] : view) { + REQUIRE(vertex_id(g, v) == 1); + } + } + + SECTION("edgelist includes all parallel edges") { + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 5); // 3 + 2 edges + } +} + +//============================================================================= +// Const Graph Tests +//============================================================================= + +TEST_CASE("Const graph - vertexlist", "[views][edge_cases][const]") { + const std::vector> g{{1, 2}, {2}, {0}}; + + auto view = g | vertexlist(); + REQUIRE(std::ranges::distance(view) == 3); + + for (auto [v] : view) { + // Should compile and work with const graph + auto id = vertex_id(g, v); + REQUIRE(id < 3); + } +} + +TEST_CASE("Const graph - incidence", "[views][edge_cases][const]") { + const std::vector> g{{1, 2}, {2}, {0}}; + + auto view = g | incidence(0); + REQUIRE(std::ranges::distance(view) == 2); + + for (auto [e] : view) { + REQUIRE(source_id(g, e) == 0); + } +} + +TEST_CASE("Const graph - neighbors", "[views][edge_cases][const]") { + const std::vector> g{{1, 2}, {2}, {0}}; + + auto view = g | neighbors(0); + REQUIRE(std::ranges::distance(view) == 2); + + for (auto [v] : view) { + auto id = vertex_id(g, v); + REQUIRE((id == 1 || id == 2)); + } +} + +TEST_CASE("Const graph - edgelist", "[views][edge_cases][const]") { + const std::vector> g{{1, 2}, {2}, {0}}; + + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 4); +} + +TEST_CASE("Const graph - topological sort (DAG)", "[views][edge_cases][const]") { + const std::vector> g{{1}, {2}, {}}; + + auto view = g | vertices_topological_sort(); + REQUIRE(std::ranges::distance(view) == 3); +} + +//============================================================================= +// Deque-Based Graph Tests (Alternative Container) +//============================================================================= + +TEST_CASE("Deque-based graph - basic views", "[views][edge_cases][deque]") { + std::deque> g(3); + g[0].push_back(1); + g[0].push_back(2); + g[1].push_back(2); + + SECTION("vertexlist") { + auto view = g | vertexlist(); + REQUIRE(std::ranges::distance(view) == 3); + } + + SECTION("incidence") { + auto view = g | incidence(0); + REQUIRE(std::ranges::distance(view) == 2); + } + + SECTION("neighbors") { + auto view = g | neighbors(0); + REQUIRE(std::ranges::distance(view) == 2); + } + + SECTION("edgelist") { + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 3); + } +} + +//============================================================================= +// Map-Based Sparse Graph Tests +//============================================================================= + +TEST_CASE("Sparse vertex IDs - non-contiguous", "[views][edge_cases][sparse]") { + // Using vector but treating as sparse + std::vector> g(11); + g[0].push_back(5); + g[5].push_back(10); + // Vertices 1-4, 6-9 exist but have no edges + + auto view = g | edgelist(); + REQUIRE(std::ranges::distance(view) == 2); + + for (auto [e] : view) { + auto sid = source_id(g, e); + auto tid = target_id(g, e); + REQUIRE((sid == 0 || sid == 5)); + REQUIRE((tid == 5 || tid == 10)); + } +} + +//============================================================================= +// Value Function Edge Cases +//============================================================================= + +TEST_CASE("Value function - capturing lambda", "[views][edge_cases][value_function]") { + std::vector> g{{1, 2}, {2}, {}}; + std::map names{{0, "A"}, {1, "B"}, {2, "C"}}; + + auto vvf = [&names, &g](auto v) { + return names[vertex_id(g, v)]; + }; + + auto view = g | vertexlist(vvf); + + for (auto [v, name] : view) { + auto id = vertex_id(g, v); + REQUIRE(name == names[id]); + } +} + +TEST_CASE("Value function - mutable lambda", "[views][edge_cases][value_function]") { + std::vector> g{{1}, {2}, {}}; + + int counter = 0; + auto vvf = [&g, counter](auto v) mutable { + return vertex_id(g, v) + counter++; + }; + + auto view = g | vertexlist(vvf); + + std::vector values; + for (auto [v, val] : view) { + values.push_back(val); + } + + // Counter should increment for each call + REQUIRE(values.size() == 3); +} + +TEST_CASE("Value function - with structured binding", "[views][edge_cases][value_function]") { + std::vector> g{{1, 2}, {2}, {}}; + + auto vvf = [&g](auto v) { + return vertex_id(g, v) * 10; + }; + + auto view = g | vertexlist(vvf); + + for (auto [v, val] : view) { + REQUIRE(val == vertex_id(g, v) * 10); + } +} + +//============================================================================= +// Exception Safety Tests +//============================================================================= + +TEST_CASE("Exception safety - value function throws", "[views][edge_cases][exception]") { + std::vector> g{{1, 2}, {2}, {}}; + + auto throwing_vvf = [&g](auto v) -> int { + auto id = vertex_id(g, v); + if (id == 1) { + throw std::runtime_error("Test exception"); + } + return static_cast(id); + }; + + auto view = g | vertexlist(throwing_vvf); + auto it = view.begin(); + + // First vertex should work + REQUIRE_NOTHROW(*it); + ++it; + + // Second vertex should throw + REQUIRE_THROWS_AS(*it, std::runtime_error); +} + +//============================================================================= +// Large Graph Stress Tests +//============================================================================= + +TEST_CASE("Large graph - vertexlist stress test", "[views][edge_cases][large]") { + constexpr size_t SIZE = 10000; + std::vector> g(SIZE); + + // Linear chain + for (size_t i = 0; i + 1 < SIZE; ++i) { + g[i].push_back(static_cast(i + 1)); + } + + auto view = g | vertexlist(); + REQUIRE(std::ranges::distance(view) == SIZE); +} + +TEST_CASE("Large graph - DFS stress test", "[views][edge_cases][large]") { + constexpr size_t SIZE = 1000; + std::vector> g(SIZE); + + // Linear chain + for (size_t i = 0; i + 1 < SIZE; ++i) { + g[i].push_back(static_cast(i + 1)); + } + + auto view = g | vertices_dfs(0); + size_t count = 0; + + for (auto [v] : view) { + (void)v; + ++count; + } + + REQUIRE(count == SIZE); +} + +TEST_CASE("Large graph - BFS stress test", "[views][edge_cases][large]") { + constexpr size_t SIZE = 1000; + std::vector> g(SIZE); + + // Star graph (center connected to all) + for (size_t i = 1; i < SIZE; ++i) { + g[0].push_back(static_cast(i)); + } + + auto view = g | vertices_bfs(0); + size_t count = 0; + + for (auto [v] : view) { + (void)v; + ++count; + } + + REQUIRE(count == SIZE); +} + +TEST_CASE("Large graph - topological sort stress test", "[views][edge_cases][large]") { + constexpr size_t SIZE = 1000; + std::vector> g(SIZE); + + // DAG: each vertex points to next 3 vertices + for (size_t i = 0; i < SIZE; ++i) { + for (size_t j = 1; j <= 3 && i + j < SIZE; ++j) { + g[i].push_back(static_cast(i + j)); + } + } + + auto view = g | vertices_topological_sort(); + std::vector order; + + for (auto [v] : view) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == SIZE); + + // Verify topological property + for (size_t u = 0; u < SIZE; ++u) { + auto pos_u = std::find(order.begin(), order.end(), u); + for (int v : g[u]) { + auto pos_v = std::find(order.begin(), order.end(), static_cast(v)); + REQUIRE(pos_u < pos_v); + } + } +} + +//============================================================================= +// Iterator Invalidation Tests +//============================================================================= + +TEST_CASE("Iterator stability - view outlives iterators", "[views][edge_cases][iterator]") { + std::vector> g{{1, 2}, {2}, {}}; + + auto view = g | vertexlist(); + auto it1 = view.begin(); + auto it2 = view.begin(); + + // Both iterators should be equal + REQUIRE(it1 == it2); + + // Advancing one shouldn't affect the other + ++it1; + REQUIRE(it1 != it2); +} + +TEST_CASE("View copy - independent iteration", "[views][edge_cases][copy]") { + std::vector> g{{1, 2}, {2}, {}}; + + auto view1 = g | vertexlist(); + auto view2 = view1; // Copy + + auto it1 = view1.begin(); + auto it2 = view2.begin(); + + // Both should start at beginning (compare IDs, not value_type) + REQUIRE(vertex_id(g, (*it1).vertex) == vertex_id(g, (*it2).vertex)); + + // Independent iteration + ++it1; + REQUIRE(vertex_id(g, (*it1).vertex) != vertex_id(g, (*it2).vertex)); +} + +//============================================================================= +// Empty Range Tests +//============================================================================= + +TEST_CASE("Empty range - graph with vertices but no edges", "[views][edge_cases][empty_range]") { + std::vector> g(5); // 5 vertices, no edges + + SECTION("edgelist is empty") { + auto view = g | edgelist(); + REQUIRE(view.begin() == view.end()); + } + + SECTION("incidence from any vertex is empty") { + for (size_t u = 0; u < 5; ++u) { + auto view = g | incidence(u); + REQUIRE(view.begin() == view.end()); + } + } + + SECTION("neighbors from any vertex is empty") { + for (size_t u = 0; u < 5; ++u) { + auto view = g | neighbors(u); + REQUIRE(view.begin() == view.end()); + } + } +} diff --git a/tests/views/test_edge_info.cpp b/tests/views/test_edge_info.cpp new file mode 100644 index 0000000..ab52958 --- /dev/null +++ b/tests/views/test_edge_info.cpp @@ -0,0 +1,405 @@ +#include +#include +#include + +using namespace graph; + +// Mock types for testing +struct mock_edge_descriptor { + int src_id; + int tgt_id; +}; + +struct mock_value { + double weight; +}; + +TEST_CASE("edge_info: all 16 specializations compile", "[edge_info]") { + SECTION("VId, Sourced=true, E, EV all present") { + edge_info ei{1, 2, mock_edge_descriptor{0, 1}, mock_value{10.5}}; + REQUIRE(ei.source_id == 1); + REQUIRE(ei.target_id == 2); + REQUIRE(ei.edge.src_id == 0); + REQUIRE(ei.edge.tgt_id == 1); + REQUIRE(ei.value.weight == 10.5); + } + + SECTION("VId, Sourced=true, E present; EV=void") { + edge_info ei{2, 3, mock_edge_descriptor{1, 2}}; + REQUIRE(ei.source_id == 2); + REQUIRE(ei.target_id == 3); + REQUIRE(ei.edge.src_id == 1); + REQUIRE(ei.edge.tgt_id == 2); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=true, EV present; E=void") { + edge_info ei{3, 4, mock_value{20.0}}; + REQUIRE(ei.source_id == 3); + REQUIRE(ei.target_id == 4); + REQUIRE(ei.value.weight == 20.0); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=true; E=void, EV=void") { + edge_info ei{4, 5}; + REQUIRE(ei.source_id == 4); + REQUIRE(ei.target_id == 5); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=false, E, EV all present") { + edge_info ei{5, mock_edge_descriptor{2, 3}, mock_value{15.5}}; + REQUIRE(ei.target_id == 5); + REQUIRE(ei.edge.src_id == 2); + REQUIRE(ei.edge.tgt_id == 3); + REQUIRE(ei.value.weight == 15.5); + } + + SECTION("VId, Sourced=false, E present; EV=void") { + edge_info ei{6, mock_edge_descriptor{3, 4}}; + REQUIRE(ei.target_id == 6); + REQUIRE(ei.edge.src_id == 3); + REQUIRE(ei.edge.tgt_id == 4); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=false, EV present; E=void") { + edge_info ei{7, mock_value{25.0}}; + REQUIRE(ei.target_id == 7); + REQUIRE(ei.value.weight == 25.0); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=false; E=void, EV=void") { + edge_info ei{8}; + REQUIRE(ei.target_id == 8); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true; E, EV present (descriptor-based)") { + edge_info ei{mock_edge_descriptor{4, 5}, mock_value{30.0}}; + REQUIRE(ei.edge.src_id == 4); + REQUIRE(ei.edge.tgt_id == 5); + REQUIRE(ei.value.weight == 30.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true, EV=void; E present") { + edge_info ei{mock_edge_descriptor{5, 6}}; + REQUIRE(ei.edge.src_id == 5); + REQUIRE(ei.edge.tgt_id == 6); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true, E=void; EV present") { + edge_info ei{mock_value{35.0}}; + REQUIRE(ei.value.weight == 35.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true; E=void, EV=void (empty)") { + edge_info ei{}; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false; E, EV present (descriptor-based)") { + edge_info ei{mock_edge_descriptor{6, 7}, mock_value{40.0}}; + REQUIRE(ei.edge.src_id == 6); + REQUIRE(ei.edge.tgt_id == 7); + REQUIRE(ei.value.weight == 40.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false, EV=void; E present") { + edge_info ei{mock_edge_descriptor{7, 8}}; + REQUIRE(ei.edge.src_id == 7); + REQUIRE(ei.edge.tgt_id == 8); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false, E=void; EV present") { + edge_info ei{mock_value{45.0}}; + REQUIRE(ei.value.weight == 45.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false; E=void, EV=void (empty)") { + edge_info ei{}; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("edge_info: structured bindings work correctly", "[edge_info]") { + SECTION("Sourced=true, all four members") { + edge_info ei{1, 2, mock_edge_descriptor{0, 1}, mock_value{10.5}}; + auto [sid, tid, e, val] = ei; + REQUIRE(sid == 1); + REQUIRE(tid == 2); + REQUIRE(e.src_id == 0); + REQUIRE(e.tgt_id == 1); + REQUIRE(val.weight == 10.5); + } + + SECTION("Sourced=false, three members") { + edge_info ei{5, mock_edge_descriptor{2, 3}, mock_value{15.5}}; + auto [tid, e, val] = ei; + REQUIRE(tid == 5); + REQUIRE(e.src_id == 2); + REQUIRE(e.tgt_id == 3); + REQUIRE(val.weight == 15.5); + } + + SECTION("Three members: source_id, target_id and edge") { + edge_info ei{2, 3, mock_edge_descriptor{1, 2}}; + auto [sid, tid, e] = ei; + REQUIRE(sid == 2); + REQUIRE(tid == 3); + REQUIRE(e.src_id == 1); + REQUIRE(e.tgt_id == 2); + } + + SECTION("Two members: target_id and value") { + edge_info ei{7, mock_value{25.0}}; + auto [tid, val] = ei; + REQUIRE(tid == 7); + REQUIRE(val.weight == 25.0); + } + + SECTION("Two members: source_id and target_id only") { + edge_info ei{4, 5}; + auto [sid, tid] = ei; + REQUIRE(sid == 4); + REQUIRE(tid == 5); + } + + SECTION("Descriptor-based: edge and value") { + edge_info ei{mock_edge_descriptor{6, 7}, mock_value{40.0}}; + auto [e, val] = ei; + REQUIRE(e.src_id == 6); + REQUIRE(e.tgt_id == 7); + REQUIRE(val.weight == 40.0); + } + + SECTION("Descriptor-based: edge only") { + edge_info ei{mock_edge_descriptor{5, 6}}; + auto [e] = ei; + REQUIRE(e.src_id == 5); + REQUIRE(e.tgt_id == 6); + } + + SECTION("Descriptor-based: value only") { + edge_info ei{mock_value{45.0}}; + auto [val] = ei; + REQUIRE(val.weight == 45.0); + } +} + +TEST_CASE("edge_info: sizeof verifies physical absence of void members", "[edge_info]") { + SECTION("Full struct vs VId=void reduces size") { + using full_t = edge_info; + using no_id_t = edge_info; + + // No source_id/target_id members should be smaller or equal (padding may prevent strict reduction) + REQUIRE(sizeof(no_id_t) <= sizeof(full_t)); + // Should be at most the size of the two members plus padding + REQUIRE(sizeof(no_id_t) <= sizeof(mock_edge_descriptor) + sizeof(mock_value) + 2*sizeof(int)); + } + + SECTION("IDs only struct (Sourced=true)") { + using ids_only_t = edge_info; + REQUIRE(sizeof(ids_only_t) == 2 * sizeof(int)); // source_id + target_id + } + + SECTION("target_id only struct (Sourced=false)") { + using id_only_t = edge_info; + REQUIRE(sizeof(id_only_t) == sizeof(size_t)); // Only target_id + } + + SECTION("Empty structs") { + using empty_sourced_t = edge_info; + using empty_unsourced_t = edge_info; + + // Empty struct has size 1 in C++ (must be distinct) + REQUIRE(sizeof(empty_sourced_t) >= 1); + REQUIRE(sizeof(empty_unsourced_t) >= 1); + } +} + +TEST_CASE("edge_info: Sourced parameter affects member presence", "[edge_info]") { + SECTION("Sourced=true has source_id and target_id") { + edge_info ei{42, 99}; + REQUIRE(ei.source_id == 42); + REQUIRE(ei.target_id == 99); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Sourced=false has only target_id") { + edge_info ei{99}; + REQUIRE(ei.target_id == 99); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("edge_info: copyable and movable", "[edge_info]") { + SECTION("Copy construction - Sourced=true") { + edge_info ei1{1, 2, mock_edge_descriptor{0, 1}, mock_value{10.5}}; + edge_info ei2 = ei1; + REQUIRE(ei2.source_id == ei1.source_id); + REQUIRE(ei2.target_id == ei1.target_id); + REQUIRE(ei2.edge.src_id == ei1.edge.src_id); + REQUIRE(ei2.value.weight == ei1.value.weight); + } + + SECTION("Move construction - Sourced=false") { + edge_info ei1{5, mock_edge_descriptor{2, 3}, mock_value{15.5}}; + edge_info ei2 = std::move(ei1); + REQUIRE(ei2.target_id == 5); + REQUIRE(ei2.edge.src_id == 2); + REQUIRE(ei2.value.weight == 15.5); + } +} + +TEST_CASE("edge_info: descriptor-based pattern primary use cases", "[edge_info]") { + SECTION("Incidence view pattern: edge_info") { + // Primary pattern for incidence views (sourced iteration) + edge_info ei{mock_edge_descriptor{10, 20}, 3.14}; + + auto [e, val] = ei; + REQUIRE(e.src_id == 10); + REQUIRE(e.tgt_id == 20); + REQUIRE(val == 3.14); + + // Verify no id members present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Edgelist view pattern: edge_info") { + // Primary pattern for edgelist views (unsourced iteration) + edge_info ei{ + mock_edge_descriptor{5, 8}, + std::string("road") + }; + + auto [e, val] = ei; + REQUIRE(e.src_id == 5); + REQUIRE(e.tgt_id == 8); + REQUIRE(val == "road"); + + // Verify no id members present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Descriptor without value function") { + edge_info ei{mock_edge_descriptor{15, 25}}; + + auto [e] = ei; + REQUIRE(e.src_id == 15); + REQUIRE(e.tgt_id == 25); + + // Verify only edge member present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("edge_info: external data pattern use case", "[edge_info]") { + SECTION("Sourced external data: source_id, target_id and value") { + edge_info ei{100, 200, 12.34}; + + auto [sid, tid, val] = ei; + REQUIRE(sid == 100); + REQUIRE(tid == 200); + REQUIRE(val == 12.34); + + // Verify source_id, target_id and value present, edge absent + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Unsourced external data: target_id and value") { + edge_info ei{42, std::string("highway")}; + + auto [tid, val] = ei; + REQUIRE(tid == 42); + REQUIRE(val == "highway"); + + // Verify target_id and value present, source_id and edge absent + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("edge_info: type traits are correct", "[edge_info]") { + SECTION("All type aliases match - Sourced=true") { + using ei_t = edge_info; + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("All type aliases match - Sourced=false") { + using ei_t = edge_info; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Void type aliases when void") { + using ei_t = edge_info; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("edge_info: copyable_edge_t alias works", "[edge_info]") { + SECTION("copyable_edge_t alias (Sourced=true)") { + using alias_t = copyable_edge_t; + using explicit_t = edge_info; + + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Alias used for sourced external data") { + copyable_edge_t ce{99, 100, 3.14}; + auto [sid, tid, val] = ce; + REQUIRE(sid == 99); + REQUIRE(tid == 100); + REQUIRE(val == 3.14); + } +} diff --git a/tests/views/test_edgelist.cpp b/tests/views/test_edgelist.cpp new file mode 100644 index 0000000..b819899 --- /dev/null +++ b/tests/views/test_edgelist.cpp @@ -0,0 +1,990 @@ +/** + * @file test_edgelist.cpp + * @brief Comprehensive tests for edgelist view + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +// ============================================================================= +// Test 1: Empty Graph +// ============================================================================= + +TEST_CASE("edgelist - empty graph", "[edgelist][empty]") { + using Graph = std::vector>; + Graph g; + + SECTION("no value function - empty iteration") { + auto elist = edgelist(g); + + REQUIRE(elist.begin() == elist.end()); + + std::size_t count = 0; + for ([[maybe_unused]] auto ei : elist) { + ++count; + } + REQUIRE(count == 0); + } + + SECTION("with value function - empty iteration") { + auto elist = edgelist(g, [](auto /*e*/) { return 42; }); + + REQUIRE(elist.begin() == elist.end()); + } +} + +// ============================================================================= +// Test 2: Graph with Vertices but No Edges +// ============================================================================= + +TEST_CASE("edgelist - vertices with no edges", "[edgelist][empty]") { + using Graph = std::vector>; + Graph g = { + {}, // vertex 0 - no edges + {}, // vertex 1 - no edges + {} // vertex 2 - no edges + }; + + SECTION("no value function") { + auto elist = edgelist(g); + + REQUIRE(elist.begin() == elist.end()); + } + + SECTION("with value function") { + auto elist = edgelist(g, [](auto /*e*/) { return 42; }); + + REQUIRE(elist.begin() == elist.end()); + } +} + +// ============================================================================= +// Test 3: Single Edge +// ============================================================================= + +TEST_CASE("edgelist - single edge", "[edgelist][single]") { + using Graph = std::vector>; + Graph g = { + {1}, // vertex 0 -> edge to 1 + {} // vertex 1 - no edges + }; + + SECTION("no value function") { + auto elist = edgelist(g); + + auto it = elist.begin(); + REQUIRE(it != elist.end()); + + auto ei = *it; + REQUIRE(source_id(g, ei.edge) == 0); + REQUIRE(target_id(g, ei.edge) == 1); + + ++it; + REQUIRE(it == elist.end()); + } + + SECTION("with value function") { + auto elist = edgelist(g, [&g](auto e) { + return static_cast(source_id(g, e)) * 100 + static_cast(target_id(g, e)); + }); + + auto ei = *elist.begin(); + REQUIRE(ei.value == 1); // 0 * 100 + 1 + } + + SECTION("structured binding - no value function") { + auto elist = edgelist(g); + + std::size_t count = 0; + for (auto [e] : elist) { + REQUIRE(source_id(g, e) == 0); + REQUIRE(target_id(g, e) == 1); + ++count; + } + REQUIRE(count == 1); + } + + SECTION("structured binding - with value function") { + auto elist = edgelist(g, [&g](auto e) { + return target_id(g, e) * 10; + }); + + for (auto [e, val] : elist) { + REQUIRE(target_id(g, e) == 1); + REQUIRE(val == 10); + } + } +} + +// ============================================================================= +// Test 4: Multiple Edges from Single Vertex +// ============================================================================= + +TEST_CASE("edgelist - multiple edges from single vertex", "[edgelist][multiple]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // vertex 0 -> edges to 1, 2, 3 + {}, + {}, + {} + }; + + SECTION("iteration") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{0, 1}); + REQUIRE(edges[1] == std::pair{0, 2}); + REQUIRE(edges[2] == std::pair{0, 3}); + } + + SECTION("with value function") { + auto elist = edgelist(g, [&g](auto e) { + return target_id(g, e); + }); + + std::vector values; + for (auto [e, val] : elist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{1, 2, 3}); + } +} + +// ============================================================================= +// Test 5: Edges from Multiple Vertices (Flattening) +// ============================================================================= + +TEST_CASE("edgelist - flattening multiple vertex edge lists", "[edgelist][flattening]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // vertex 0 -> edges to 1, 2 + {2, 3}, // vertex 1 -> edges to 2, 3 + {3}, // vertex 2 -> edge to 3 + {} // vertex 3 - no edges + }; + + SECTION("all edges in order") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + // Edges should come in vertex order, then edge order within vertex + REQUIRE(edges.size() == 5); + REQUIRE(edges[0] == std::pair{0, 1}); + REQUIRE(edges[1] == std::pair{0, 2}); + REQUIRE(edges[2] == std::pair{1, 2}); + REQUIRE(edges[3] == std::pair{1, 3}); + REQUIRE(edges[4] == std::pair{2, 3}); + } + + SECTION("with value function computing edge weight") { + auto elist = edgelist(g, [&g](auto e) { + // Compute edge "weight" as source + target + return static_cast(source_id(g, e)) + static_cast(target_id(g, e)); + }); + + std::vector weights; + for (auto [e, w] : elist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{1, 2, 3, 4, 5}); + } +} + +// ============================================================================= +// Test 6: Skipping Empty Vertices +// ============================================================================= + +TEST_CASE("edgelist - skipping empty vertices", "[edgelist][skip]") { + using Graph = std::vector>; + Graph g = { + {}, // vertex 0 - no edges + {2}, // vertex 1 -> edge to 2 + {}, // vertex 2 - no edges + {}, // vertex 3 - no edges + {5}, // vertex 4 -> edge to 5 + {} // vertex 5 - no edges + }; + + SECTION("correctly skips empty vertices") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 2); + REQUIRE(edges[0] == std::pair{1, 2}); + REQUIRE(edges[1] == std::pair{4, 5}); + } +} + +// ============================================================================= +// Test 7: Value Function Types +// ============================================================================= + +TEST_CASE("edgelist - value function types", "[edgelist][evf]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {}, + {} + }; + + SECTION("returning string") { + auto elist = edgelist(g, [&g](auto e) { + return std::to_string(source_id(g, e)) + "->" + + std::to_string(target_id(g, e)); + }); + + std::vector labels; + for (auto [e, label] : elist) { + labels.push_back(label); + } + + REQUIRE(labels == std::vector{"0->1", "0->2"}); + } + + SECTION("returning double") { + auto elist = edgelist(g, [&g](auto e) { + return static_cast(target_id(g, e)) * 1.5; + }); + + std::vector values; + for (auto [e, val] : elist) { + values.push_back(val); + } + + REQUIRE(values[0] == 1.5); + REQUIRE(values[1] == 3.0); + } + + SECTION("capturing lambda") { + int multiplier = 100; + auto elist = edgelist(g, [&g, multiplier](auto e) { + return target_id(g, e) * multiplier; + }); + + std::vector values; + for (auto [e, val] : elist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{100, 200}); + } +} + +// ============================================================================= +// Test 8: Range Algorithms +// ============================================================================= + +TEST_CASE("edgelist - range algorithms", "[edgelist][algorithm]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, + {2}, + {3}, + {} + }; + + SECTION("std::ranges::distance") { + auto elist = edgelist(g); + auto count = std::ranges::distance(elist); + REQUIRE(count == 5); + } + + SECTION("std::ranges::count_if") { + auto elist = edgelist(g); + auto count = std::ranges::count_if(elist, [&g](auto ei) { + return target_id(g, ei.edge) == 3; + }); + REQUIRE(count == 2); // 0->3 and 2->3 + } + + SECTION("std::ranges::for_each") { + auto elist = edgelist(g); + int sum = 0; + std::ranges::for_each(elist, [&g, &sum](auto ei) { + sum += target_id(g, ei.edge); + }); + REQUIRE(sum == 11); // 1+2+3+2+3 + } + + SECTION("std::ranges::find_if") { + auto elist = edgelist(g); + auto it = std::ranges::find_if(elist, [&g](auto ei) { + return source_id(g, ei.edge) == 1 && target_id(g, ei.edge) == 2; + }); + REQUIRE(it != elist.end()); + REQUIRE(source_id(g, (*it).edge) == 1); + REQUIRE(target_id(g, (*it).edge) == 2); + } +} + +// ============================================================================= +// Test 9: Vector of Deques +// ============================================================================= + +TEST_CASE("edgelist - vector of deques", "[edgelist][container]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + SECTION("iteration") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{0, 1}); + REQUIRE(edges[1] == std::pair{0, 2}); + REQUIRE(edges[2] == std::pair{1, 2}); + } + + SECTION("with value function") { + auto elist = edgelist(g, [&g](auto e) { + return target_id(g, e) * 10; + }); + + std::vector values; + for (auto [e, val] : elist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{10, 20, 20}); + } +} + +// ============================================================================= +// Test 10: Deque of Vectors +// ============================================================================= + +TEST_CASE("edgelist - deque of vectors", "[edgelist][container]") { + using Graph = std::deque>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + SECTION("iteration") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 3); + } +} + +// ============================================================================= +// Test 11: Iterator Operations +// ============================================================================= + +TEST_CASE("edgelist - iterator operations", "[edgelist][iterator]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + SECTION("post-increment") { + auto elist = edgelist(g); + auto it = elist.begin(); + + auto old_it = it++; + REQUIRE(target_id(g, (*old_it).edge) == 1); + REQUIRE(target_id(g, (*it).edge) == 2); + } + + SECTION("equality comparison") { + auto elist = edgelist(g); + auto it1 = elist.begin(); + auto it2 = elist.begin(); + + REQUIRE(it1 == it2); + ++it1; + REQUIRE(it1 != it2); + } + + SECTION("end iterator comparison") { + auto elist = edgelist(g); + auto it = elist.begin(); + + // 3 edges total + ++it; ++it; ++it; + REQUIRE(it == elist.end()); + } +} + +// ============================================================================= +// Test 12: Range Concepts +// ============================================================================= + +TEST_CASE("edgelist - satisfies range concepts", "[edgelist][concepts]") { + using Graph = std::vector>; + Graph g = {{1}, {2}, {}}; + + SECTION("view without value function") { + auto elist = edgelist(g); + + STATIC_REQUIRE(std::ranges::range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::view); + } + + SECTION("view with value function") { + auto elist = edgelist(g, [](auto /*e*/) { return 42; }); + + STATIC_REQUIRE(std::ranges::range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::view); + } +} + +// ============================================================================= +// Test 13: Map-Based Vertex Container +// ============================================================================= + +TEST_CASE("edgelist - map-based vertex container", "[edgelist][map]") { + // Map vertices - non-contiguous vertex IDs + using Graph = std::map>; + Graph g = { + {100, {200, 300}}, // vertex 100 -> edges to 200, 300 + {200, {300}}, // vertex 200 -> edge to 300 + {300, {}} // vertex 300 - no edges + }; + + SECTION("iteration over all edges") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{100, 200}); + REQUIRE(edges[1] == std::pair{100, 300}); + REQUIRE(edges[2] == std::pair{200, 300}); + } + + SECTION("with value function") { + auto elist = edgelist(g, [&g](auto e) { + return target_id(g, e) - source_id(g, e); + }); + + std::vector diffs; + for (auto [e, diff] : elist) { + diffs.push_back(diff); + } + + REQUIRE(diffs == std::vector{100, 200, 100}); + } + + SECTION("empty edge list") { + Graph empty_g = { + {10, {}}, + {20, {}}, + {30, {}} + }; + + auto elist = edgelist(empty_g); + REQUIRE(elist.begin() == elist.end()); + } +} + +// ============================================================================= +// Test 14: Map-Based Edge Container (Sorted Edges) +// ============================================================================= + +TEST_CASE("edgelist - vector vertices map edges", "[edgelist][edge_map]") { + // Edges stored in map (sorted by target, with edge values) + using Graph = std::vector>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 -> (1, 1.5), (2, 2.5) + {{2, 3.5}}, // vertex 1 -> (2, 3.5) + {} // vertex 2 -> no edges + }; + + SECTION("iteration") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{0, 1}); + REQUIRE(edges[1] == std::pair{0, 2}); + REQUIRE(edges[2] == std::pair{1, 2}); + } + + SECTION("accessing edge weights via edge_value") { + auto elist = edgelist(g, [&g](auto e) { + return edge_value(g, e); + }); + + std::vector weights; + for (auto [e, w] : elist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{1.5, 2.5, 3.5}); + } +} + +// ============================================================================= +// Test 15: Map Vertices + Map Edges (Fully Sparse Graph) +// ============================================================================= + +TEST_CASE("edgelist - map vertices map edges", "[edgelist][map][edge_map]") { + // Both vertices and edges in maps - fully sparse graph + using Graph = std::map>; + Graph g = { + {10, {{20, 1.0}, {30, 2.0}}}, // vertex 10 -> (20, 1.0), (30, 2.0) + {20, {{30, 3.0}}}, // vertex 20 -> (30, 3.0) + {30, {}} // vertex 30 -> no edges + }; + + SECTION("iteration over all edges") { + auto elist = edgelist(g); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{10, 20}); + REQUIRE(edges[1] == std::pair{10, 30}); + REQUIRE(edges[2] == std::pair{20, 30}); + } + + SECTION("with edge value function") { + auto elist = edgelist(g, [&g](auto e) { + return edge_value(g, e); + }); + + std::vector weights; + for (auto [e, w] : elist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{1.0, 2.0, 3.0}); + } + + SECTION("combined source, target, weight extraction") { + auto elist = edgelist(g, [&g](auto e) { + return edge_value(g, e); + }); + + std::vector> all_edges; + for (auto [e, w] : elist) { + all_edges.emplace_back(source_id(g, e), target_id(g, e), w); + } + + REQUIRE(all_edges.size() == 3); + REQUIRE(std::get<0>(all_edges[0]) == 10); + REQUIRE(std::get<1>(all_edges[0]) == 20); + REQUIRE(std::get<2>(all_edges[0]) == 1.0); + + REQUIRE(std::get<0>(all_edges[2]) == 20); + REQUIRE(std::get<1>(all_edges[2]) == 30); + REQUIRE(std::get<2>(all_edges[2]) == 3.0); + } +} + +// ============================================================================= +// ============================================================================= +// EDGE_LIST TESTS (Step 2.4.1) +// ============================================================================= +// ============================================================================= + +// ============================================================================= +// Test 16: edge_list with pairs +// ============================================================================= + +TEST_CASE("edgelist - edge_list with pairs", "[edgelist][edge_list]") { + using EdgeList = std::vector>; + EdgeList el = {{1, 2}, {2, 3}, {3, 4}, {4, 5}}; + + SECTION("no value function") { + auto elist = edgelist(el); + + REQUIRE(elist.size() == 4); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(el, e), target_id(el, e)); + } + + REQUIRE(edges.size() == 4); + REQUIRE(edges[0] == std::pair{1, 2}); + REQUIRE(edges[1] == std::pair{2, 3}); + REQUIRE(edges[2] == std::pair{3, 4}); + REQUIRE(edges[3] == std::pair{4, 5}); + } + + SECTION("with value function") { + auto elist = edgelist(el, [](auto& el, auto e) { + return source_id(el, e) + target_id(el, e); + }); + + std::vector sums; + for (auto [e, sum] : elist) { + sums.push_back(sum); + } + + REQUIRE(sums == std::vector{3, 5, 7, 9}); + } +} + +// ============================================================================= +// Test 17: edge_list with tuples (2-tuples, no value) +// ============================================================================= + +TEST_CASE("edgelist - edge_list with 2-tuples", "[edgelist][edge_list]") { + using EdgeList = std::vector>; + EdgeList el = {{0, 1}, {1, 2}, {2, 0}}; + + SECTION("iteration") { + auto elist = edgelist(el); + + REQUIRE(elist.size() == 3); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(el, e), target_id(el, e)); + } + + REQUIRE(edges[0] == std::pair{0, 1}); + REQUIRE(edges[1] == std::pair{1, 2}); + REQUIRE(edges[2] == std::pair{2, 0}); + } +} + +// ============================================================================= +// Test 18: edge_list with 3-tuples (weighted edges) +// ============================================================================= + +TEST_CASE("edgelist - edge_list with 3-tuples (weighted)", "[edgelist][edge_list]") { + using EdgeList = std::vector>; + EdgeList el = {{0, 1, 1.5}, {1, 2, 2.5}, {2, 3, 3.5}}; + + SECTION("no value function") { + auto elist = edgelist(el); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(el, e), target_id(el, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{0, 1}); + REQUIRE(edges[2] == std::pair{2, 3}); + } + + SECTION("value function accessing edge_value") { + auto elist = edgelist(el, [](auto& el, auto e) { + return edge_value(el, e); + }); + + std::vector weights; + for (auto [e, w] : elist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{1.5, 2.5, 3.5}); + } + + SECTION("value function computing derived value") { + auto elist = edgelist(el, [](auto& el, auto e) { + return edge_value(el, e) * 2.0; + }); + + std::vector doubled; + for (auto [e, val] : elist) { + doubled.push_back(val); + } + + REQUIRE(doubled == std::vector{3.0, 5.0, 7.0}); + } +} + +// ============================================================================= +// Test 19: edge_list with edge_info +// ============================================================================= + +TEST_CASE("edgelist - edge_list with edge_info", "[edgelist][edge_list]") { + using EdgeType = edge_info; + using EdgeList = std::vector; + + EdgeList el = { + EdgeType{10, 20}, + EdgeType{20, 30}, + EdgeType{30, 40} + }; + + SECTION("no value function") { + auto elist = edgelist(el); + + REQUIRE(elist.size() == 3); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(el, e), target_id(el, e)); + } + + REQUIRE(edges[0] == std::pair{10, 20}); + REQUIRE(edges[1] == std::pair{20, 30}); + REQUIRE(edges[2] == std::pair{30, 40}); + } + + SECTION("with value function") { + auto elist = edgelist(el, [](auto& el, auto e) { + return target_id(el, e) - source_id(el, e); + }); + + std::vector diffs; + for (auto [e, diff] : elist) { + diffs.push_back(diff); + } + + REQUIRE(diffs == std::vector{10, 10, 10}); + } +} + +// ============================================================================= +// Test 20: edge_list with edge_info (with value) +// ============================================================================= + +TEST_CASE("edgelist - edge_list with edge_info with value", "[edgelist][edge_list]") { + using EdgeType = edge_info; + using EdgeList = std::vector; + + EdgeList el = { + EdgeType{1, 2, 0.5}, + EdgeType{2, 3, 1.5}, + EdgeType{3, 1, 2.5} + }; + + SECTION("accessing edge_value") { + auto elist = edgelist(el, [](auto& el, auto e) { + return edge_value(el, e); + }); + + std::vector weights; + for (auto [e, w] : elist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{0.5, 1.5, 2.5}); + } +} + +// ============================================================================= +// Test 21: Empty edge_list +// ============================================================================= + +TEST_CASE("edgelist - empty edge_list", "[edgelist][edge_list][empty]") { + using EdgeList = std::vector>; + EdgeList el; + + SECTION("no value function") { + auto elist = edgelist(el); + + REQUIRE(elist.size() == 0); + REQUIRE(elist.begin() == elist.end()); + } + + SECTION("with value function") { + auto elist = edgelist(el, [](auto& /*el*/, auto /*e*/) { return 42; }); + + REQUIRE(elist.begin() == elist.end()); + } +} + +// ============================================================================= +// Test 22: edge_list range concepts +// ============================================================================= + +TEST_CASE("edgelist - edge_list satisfies range concepts", "[edgelist][edge_list][concepts]") { + using EdgeList = std::vector>; + EdgeList el = {{1, 2}, {3, 4}}; + + SECTION("view without value function") { + auto elist = edgelist(el); + + STATIC_REQUIRE(std::ranges::range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::view); + STATIC_REQUIRE(std::ranges::sized_range); + } + + SECTION("view with value function") { + auto elist = edgelist(el, [](auto& el, auto e) { return source_id(el, e); }); + + STATIC_REQUIRE(std::ranges::range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::view); + STATIC_REQUIRE(std::ranges::sized_range); + } +} + +// ============================================================================= +// Test 23: edge_list iterator operations +// ============================================================================= + +TEST_CASE("edgelist - edge_list iterator operations", "[edgelist][edge_list][iterator]") { + using EdgeList = std::vector>; + EdgeList el = {{1, 2}, {2, 3}, {3, 4}}; + + SECTION("post-increment") { + auto elist = edgelist(el); + auto it = elist.begin(); + + auto old_it = it++; + REQUIRE(target_id(el, (*old_it).edge) == 2); + REQUIRE(target_id(el, (*it).edge) == 3); + } + + SECTION("equality comparison") { + auto elist = edgelist(el); + auto it1 = elist.begin(); + auto it2 = elist.begin(); + + REQUIRE(it1 == it2); + ++it1; + REQUIRE(it1 != it2); + } + + SECTION("end iterator comparison") { + auto elist = edgelist(el); + auto it = elist.begin(); + + ++it; ++it; ++it; + REQUIRE(it == elist.end()); + } +} + +// ============================================================================= +// Test 24: edge_list with string vertex IDs +// ============================================================================= + +TEST_CASE("edgelist - edge_list with string vertex IDs", "[edgelist][edge_list][string]") { + using EdgeList = std::vector>; + EdgeList el = {{"A", "B"}, {"B", "C"}, {"C", "A"}}; + + SECTION("iteration") { + auto elist = edgelist(el); + + REQUIRE(elist.size() == 3); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(el, e), target_id(el, e)); + } + + REQUIRE(edges[0] == std::pair{"A", "B"}); + REQUIRE(edges[1] == std::pair{"B", "C"}); + REQUIRE(edges[2] == std::pair{"C", "A"}); + } + + SECTION("with value function creating labels") { + auto elist = edgelist(el, [](auto& el, auto e) { + return source_id(el, e) + "->" + target_id(el, e); + }); + + std::vector labels; + for (auto [e, label] : elist) { + labels.push_back(label); + } + + REQUIRE(labels == std::vector{"A->B", "B->C", "C->A"}); + } +} + +// ============================================================================= +// Test 25: edge_list with range algorithms +// ============================================================================= + +TEST_CASE("edgelist - edge_list with range algorithms", "[edgelist][edge_list][algorithm]") { + using EdgeList = std::vector>; + EdgeList el = {{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}}; + + SECTION("std::ranges::distance") { + auto elist = edgelist(el); + REQUIRE(std::ranges::distance(elist) == 5); + } + + SECTION("std::ranges::count_if") { + auto elist = edgelist(el); + auto count = std::ranges::count_if(elist, [&el](auto ei) { + return target_id(el, ei.edge) > 3; + }); + REQUIRE(count == 3); // edges to 4, 5, 6 + } + + SECTION("std::ranges::for_each") { + auto elist = edgelist(el); + int sum = 0; + std::ranges::for_each(elist, [&el, &sum](auto ei) { + sum += target_id(el, ei.edge); + }); + REQUIRE(sum == 20); // 2+3+4+5+6 + } +} + +// ============================================================================= +// Test 26: Deque-based edge_list +// ============================================================================= + +TEST_CASE("edgelist - deque-based edge_list", "[edgelist][edge_list][container]") { + using EdgeList = std::deque>; + EdgeList el = {{1, 2}, {2, 3}, {3, 4}}; + + SECTION("iteration") { + auto elist = edgelist(el); + + std::vector> edges; + for (auto [e] : elist) { + edges.emplace_back(source_id(el, e), target_id(el, e)); + } + + REQUIRE(edges.size() == 3); + REQUIRE(edges[0] == std::pair{1, 2}); + } +} diff --git a/tests/views/test_graph_hpp_includes_views.cpp b/tests/views/test_graph_hpp_includes_views.cpp new file mode 100644 index 0000000..0ef017b --- /dev/null +++ b/tests/views/test_graph_hpp_includes_views.cpp @@ -0,0 +1,149 @@ +/** + * @file test_graph_hpp_includes_views.cpp + * @brief Tests that the graph views library works correctly with graph.hpp + * + * This test verifies that users can access all views by including both + * and . While the view syntax would + * prefer to have everything in one header, circular dependencies between + * graph.hpp and views.hpp make this impractical. + */ + +#include + +// Include the main graph.hpp header +#include +// Include views - required separately to avoid circular dependencies +#include + +#include +#include + +using namespace graph; +using namespace graph::views::adaptors; + +// Simple test graph using vector-of-vectors +// Graph structure: 0 -> {1, 2}, 1 -> {2}, 2 -> {} +using test_graph = std::vector>; + +inline auto make_test_graph() { + test_graph g(3); // 3 vertices + g[0] = {1, 2}; // vertex 0 connects to vertices 1 and 2 + g[1] = {2}; // vertex 1 connects to vertex 2 + g[2] = {}; // vertex 2 has no outgoing edges + return g; +} + +TEST_CASE("graph.hpp - basic views accessible", "[graph_hpp][basic_views]") { + auto g = make_test_graph(); + + // Test that basic views work through graph.hpp + int vertex_count = 0; + for (auto [v] : g | vertexlist()) { + ++vertex_count; + } + REQUIRE(vertex_count == 3); + + int edge_count = 0; + for (auto [e] : g | incidence(0)) { + ++edge_count; + } + REQUIRE(edge_count == 2); + + int neighbor_count = 0; + for (auto [n] : g | neighbors(0)) { + ++neighbor_count; + } + REQUIRE(neighbor_count == 2); + + int total_edges = 0; + for (auto [e] : g | edgelist()) { + ++total_edges; + } + REQUIRE(total_edges == 3); +} + +TEST_CASE("graph.hpp - search views accessible", "[graph_hpp][search_views]") { + auto g = make_test_graph(); + + // Test that DFS views work through graph.hpp + int dfs_vertices = 0; + for (auto [v] : g | vertices_dfs(0)) { + ++dfs_vertices; + } + REQUIRE(dfs_vertices == 3); + + int dfs_edges = 0; + for (auto [e] : g | edges_dfs(0)) { + ++dfs_edges; + } + REQUIRE(dfs_edges == 2); // DFS tree has 2 edges + + // Test that BFS views work through graph.hpp + int bfs_vertices = 0; + for (auto [v] : g | vertices_bfs(0)) { + ++bfs_vertices; + } + REQUIRE(bfs_vertices == 3); + + int bfs_edges = 0; + for (auto [e] : g | edges_bfs(0)) { + ++bfs_edges; + } + REQUIRE(bfs_edges == 2); // BFS tree also has 2 edges + + // Test that topological sort views work through graph.hpp + int topo_vertices = 0; + for (auto [v] : g | vertices_topological_sort()) { + ++topo_vertices; + } + REQUIRE(topo_vertices == 3); + + int topo_edges = 0; + for (auto [e] : g | edges_topological_sort()) { + ++topo_edges; + } + REQUIRE(topo_edges == 3); +} + +TEST_CASE("graph.hpp - value functions work", "[graph_hpp][value_functions]") { + auto g = make_test_graph(); + + // Test that value functions work through graph.hpp + auto vvf = [&g](auto v) { return vertex_id(g, v); }; + + std::vector values; + for (auto [v, val] : g | vertexlist(vvf)) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + // Values should be vertex IDs 0, 1, 2 + std::sort(values.begin(), values.end()); + REQUIRE(values == std::vector{0, 1, 2}); +} + +TEST_CASE("graph.hpp - chaining with std::views", "[graph_hpp][chaining]") { + auto g = make_test_graph(); + + // Test that chaining with std::views works through graph.hpp + auto vertex_view = g + | vertexlist() + | std::views::take(2); + + int count = 0; + for (auto [v] : vertex_view) { + ++count; + } + REQUIRE(count == 2); + + // Test complex chain + auto dfs_view = g + | vertices_dfs(0) + | std::views::drop(1); + + count = 0; + for (auto [v] : dfs_view) { + ++count; + } + REQUIRE(count == 2); // 3 vertices - 1 dropped = 2 +} diff --git a/tests/views/test_incidence.cpp b/tests/views/test_incidence.cpp new file mode 100644 index 0000000..66b26fe --- /dev/null +++ b/tests/views/test_incidence.cpp @@ -0,0 +1,1227 @@ +/** + * @file test_incidence.cpp + * @brief Comprehensive tests for incidence view + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +// ============================================================================= +// Test 1: Empty Vertex (No Edges) +// ============================================================================= + +TEST_CASE("incidence - vertex with no edges", "[incidence][empty]") { + using Graph = std::vector>; + Graph g = { + {}, // vertex 0 - no edges + {0}, // vertex 1 - edge to 0 + {0, 1} // vertex 2 - edges to 0 and 1 + }; + + SECTION("no value function - empty iteration") { + auto v0 = vertex_t{0}; // vertex 0 has no edges + auto ilist = incidence(g, v0); + + REQUIRE(ilist.begin() == ilist.end()); + REQUIRE(ilist.size() == 0); + } + + SECTION("with value function - empty iteration") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0, [](auto /*e*/) { return 42; }); + + REQUIRE(ilist.begin() == ilist.end()); + } +} + +// ============================================================================= +// Test 2: Single Edge +// ============================================================================= + +TEST_CASE("incidence - vertex with single edge", "[incidence][single]") { + using Graph = std::vector>; + Graph g = { + {1}, // vertex 0 -> edge to 1 + {} // vertex 1 - no edges + }; + + SECTION("no value function") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0); + + REQUIRE(ilist.size() == 1); + + auto it = ilist.begin(); + REQUIRE(it != ilist.end()); + + auto ei = *it; + // edge_info, void> has just an 'edge' member + // For vov graph, edge_t is an edge_descriptor + auto target = target_id(g, ei.edge); + REQUIRE(target == 1); + + ++it; + REQUIRE(it == ilist.end()); + } + + SECTION("with value function") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0, [&g](auto e) { + return target_id(g, e) * 10; + }); + + REQUIRE(ilist.size() == 1); + + auto ei = *ilist.begin(); + REQUIRE(ei.value == 10); // target_id(1) * 10 + } +} + +// ============================================================================= +// Test 3: Multiple Edges +// ============================================================================= + +TEST_CASE("incidence - vertex with multiple edges", "[incidence][multiple]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // vertex 0 -> edges to 1, 2, 3 + {2, 3}, // vertex 1 -> edges to 2, 3 + {3}, // vertex 2 -> edge to 3 + {} // vertex 3 - no edges + }; + + SECTION("no value function - iteration") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0); + + REQUIRE(ilist.size() == 3); + + std::vector targets; + for (auto ei : ilist) { + targets.push_back(target_id(g, ei.edge)); + } + + REQUIRE(targets == std::vector{1, 2, 3}); + } + + SECTION("with value function") { + auto v1 = vertex_t{1}; + auto ilist = incidence(g, v1, [&g](auto e) { + return static_cast(target_id(g, e) * 100); + }); + + std::vector values; + for (auto ei : ilist) { + values.push_back(ei.value); + } + + REQUIRE(values == std::vector{200, 300}); + } + + SECTION("structured binding - no value function") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + REQUIRE(targets == std::vector{1, 2, 3}); + } + + SECTION("structured binding - with value function") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0, [&g](auto e) { + return target_id(g, e) + 100; + }); + + std::vector edge_targets; + std::vector values; + for (auto [e, val] : ilist) { + edge_targets.push_back(target_id(g, e)); + values.push_back(val); + } + + REQUIRE(edge_targets == std::vector{1, 2, 3}); + REQUIRE(values == std::vector{101, 102, 103}); + } +} + +// ============================================================================= +// Test 4: Value Function Types +// ============================================================================= + +TEST_CASE("incidence - value function types", "[incidence][evf]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // vertex 0 -> edges to 1, 2 + {}, + {} + }; + auto v0 = vertex_t{0}; + + SECTION("returning string") { + auto ilist = incidence(g, v0, [&g](auto e) { + return "edge_to_" + std::to_string(target_id(g, e)); + }); + + std::vector names; + for (auto [e, name] : ilist) { + names.push_back(name); + } + + REQUIRE(names == std::vector{"edge_to_1", "edge_to_2"}); + } + + SECTION("returning double") { + auto ilist = incidence(g, v0, [&g](auto e) { + return static_cast(target_id(g, e)) * 1.5; + }); + + std::vector values; + for (auto [e, val] : ilist) { + values.push_back(val); + } + + REQUIRE(values[0] == 1.5); + REQUIRE(values[1] == 3.0); + } + + SECTION("capturing lambda") { + int multiplier = 7; + auto ilist = incidence(g, v0, [&g, multiplier](auto e) { + return static_cast(target_id(g, e) * multiplier); + }); + + std::vector values; + for (auto [e, val] : ilist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{7, 14}); + } +} + +// ============================================================================= +// Test 5: Edge Descriptor Access +// ============================================================================= + +TEST_CASE("incidence - edge descriptor access", "[incidence][descriptor]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // vertex 0 -> edges to 1, 2, 3 + {}, + {}, + {} + }; + auto v0 = vertex_t{0}; + + SECTION("source_id access") { + auto ilist = incidence(g, v0); + + for (auto [e] : ilist) { + // Every edge from v0 should have source_id == 0 + REQUIRE(source_id(g, e) == 0); + } + } + + SECTION("target_id access") { + auto ilist = incidence(g, v0); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + + REQUIRE(targets == std::vector{1, 2, 3}); + } +} + +// ============================================================================= +// Test 6: Weighted Graph (Pair Edges) +// ============================================================================= + +TEST_CASE("incidence - weighted graph", "[incidence][weighted]") { + // Graph with weighted edges: vector>> + using Graph = std::vector>>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 -> (1, 1.5), (2, 2.5) + {{2, 3.5}}, // vertex 1 -> (2, 3.5) + {} + }; + + SECTION("no value function") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0); + + REQUIRE(ilist.size() == 2); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + + REQUIRE(targets == std::vector{1, 2}); + } + + SECTION("value function accessing edge weight") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0, [&g](auto e) { + return edge_value(g, e); // Get the weight + }); + + std::vector weights; + for (auto [e, w] : ilist) { + weights.push_back(w); + } + + REQUIRE(weights[0] == 1.5); + REQUIRE(weights[1] == 2.5); + } +} + +// ============================================================================= +// Test 7: Range Concepts +// ============================================================================= + +TEST_CASE("incidence - range concepts", "[incidence][concepts]") { + using Graph = std::vector>; + Graph g = {{1, 2}, {}, {}}; + auto v0 = vertex_t{0}; + + SECTION("no value function") { + auto ilist = incidence(g, v0); + + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::sized_range); + STATIC_REQUIRE(std::ranges::view); + } + + SECTION("with value function") { + auto ilist = incidence(g, v0, [&g](auto e) { return target_id(g, e); }); + + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::sized_range); + // Note: view concept may not be satisfied if EVF is not assignable (e.g., lambda) + // The view_base is inherited, but movable is required + } +} + +// ============================================================================= +// Test 8: Iterator Properties +// ============================================================================= + +TEST_CASE("incidence - iterator properties", "[incidence][iterator]") { + using Graph = std::vector>; + Graph g = {{1, 2, 3}, {}, {}, {}}; + auto v0 = vertex_t{0}; + + SECTION("pre-increment returns reference") { + auto ilist = incidence(g, v0); + auto it = ilist.begin(); + auto& ref = ++it; + REQUIRE(&ref == &it); + } + + SECTION("post-increment returns copy") { + auto ilist = incidence(g, v0); + auto it = ilist.begin(); + auto copy = it++; + REQUIRE(copy != it); + } + + SECTION("equality comparison") { + auto ilist = incidence(g, v0); + auto it1 = ilist.begin(); + auto it2 = ilist.begin(); + REQUIRE(it1 == it2); + ++it1; + REQUIRE(it1 != it2); + } +} + +// ============================================================================= +// Test 9: edge_info Type Verification +// ============================================================================= + +TEST_CASE("incidence - edge_info type verification", "[incidence][types]") { + using Graph = std::vector>; + using EdgeType = edge_t; + + SECTION("no value function - edge_info") { + using ViewType = incidence_view; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + + // Verify edge_info members + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("with value function - edge_info") { + using VVF = int(*)(EdgeType); + using ViewType = incidence_view; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + + // Verify edge_info members + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +// ============================================================================= +// Test 10: std::ranges Algorithms +// ============================================================================= + +TEST_CASE("incidence - std::ranges algorithms", "[incidence][algorithms]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3, 4, 5}, // vertex 0 -> edges to 1-5 + {}, + {}, + {}, + {}, + {} + }; + auto v0 = vertex_t{0}; + + SECTION("std::ranges::distance") { + auto ilist = incidence(g, v0); + REQUIRE(std::ranges::distance(ilist) == 5); + } + + SECTION("std::ranges::count_if") { + auto ilist = incidence(g, v0); + auto count = std::ranges::count_if(ilist, [&g](auto& ei) { + return target_id(g, ei.edge) > 2; + }); + REQUIRE(count == 3); // targets 3, 4, 5 + } +} + +// ============================================================================= +// Test 11: Deque-based Graph +// ============================================================================= + +TEST_CASE("incidence - deque-based graph", "[incidence][deque]") { + using Graph = std::deque>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + + REQUIRE(targets == std::vector{1, 2}); +} + +// ============================================================================= +// Test 12: All Vertices Iteration +// ============================================================================= + +TEST_CASE("incidence - iterating all vertices", "[incidence][all]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // vertex 0 -> 1, 2 + {2}, // vertex 1 -> 2 + {} // vertex 2 -> no edges + }; + + // Collect all edges from all vertices + std::vector> all_edges; + + for (auto [v] : vertexlist(g)) { + for (auto [e] : incidence(g, v)) { + all_edges.emplace_back(source_id(g, e), target_id(g, e)); + } + } + + REQUIRE(all_edges.size() == 3); + REQUIRE(all_edges[0] == std::pair{0, 1}); + REQUIRE(all_edges[1] == std::pair{0, 2}); + REQUIRE(all_edges[2] == std::pair{1, 2}); +} + +// ============================================================================= +// Test 13: Map-Based Vertex Container (Sparse Vertex IDs) +// ============================================================================= + +TEST_CASE("incidence - map vertices vector edges", "[incidence][map]") { + // Map-based graphs have sparse, non-contiguous vertex IDs + using Graph = std::map>; + Graph g = { + {100, {200, 300}}, // vertex 100 -> edges to 200, 300 + {200, {300}}, // vertex 200 -> edge to 300 + {300, {}} // vertex 300 -> no edges + }; + + SECTION("iteration over edges from sparse vertex") { + // Get vertex 100 + auto verts = vertices(g); + auto v100 = *verts.begin(); + REQUIRE(v100.vertex_id() == 100); + + auto ilist = incidence(g, v100); + + REQUIRE(ilist.size() == 2); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + + REQUIRE(targets == std::vector{200, 300}); + } + + SECTION("source_id is correct for map vertex") { + auto verts = vertices(g); + auto v100 = *verts.begin(); + + for (auto [e] : incidence(g, v100)) { + REQUIRE(source_id(g, e) == 100); + } + } + + SECTION("empty edge list") { + auto verts = vertices(g); + auto it = verts.begin(); + std::advance(it, 2); // Get vertex 300 + auto v300 = *it; + REQUIRE(v300.vertex_id() == 300); + + auto ilist = incidence(g, v300); + REQUIRE(ilist.size() == 0); + REQUIRE(ilist.begin() == ilist.end()); + } + + SECTION("with value function") { + auto verts = vertices(g); + auto v100 = *verts.begin(); + + auto ilist = incidence(g, v100, [&g](auto e) { + return target_id(g, e) - 100; // Offset from base + }); + + std::vector offsets; + for (auto [e, offset] : ilist) { + offsets.push_back(offset); + } + + REQUIRE(offsets == std::vector{100, 200}); // 200-100=100, 300-100=200 + } + + SECTION("iterate all vertices and edges") { + std::vector> all_edges; + + for (auto [v] : vertexlist(g)) { + for (auto [e] : incidence(g, v)) { + all_edges.emplace_back(source_id(g, e), target_id(g, e)); + } + } + + REQUIRE(all_edges.size() == 3); + REQUIRE(all_edges[0] == std::pair{100, 200}); + REQUIRE(all_edges[1] == std::pair{100, 300}); + REQUIRE(all_edges[2] == std::pair{200, 300}); + } +} + +// ============================================================================= +// Test 14: Map-Based Edge Container (Sorted Edges) +// ============================================================================= + +TEST_CASE("incidence - vector vertices map edges", "[incidence][edge_map]") { + // Edges stored in map (sorted by target, with edge values) + using Graph = std::vector>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 -> (1, 1.5), (2, 2.5) + {{2, 3.5}}, // vertex 1 -> (2, 3.5) + {} // vertex 2 -> no edges + }; + + SECTION("iteration") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0); + + REQUIRE(ilist.size() == 2); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + + // Map edges are sorted by target_id (key) + REQUIRE(targets == std::vector{1, 2}); + } + + SECTION("accessing edge weights via edge_value") { + auto v0 = vertex_t{0}; + auto ilist = incidence(g, v0, [&g](auto e) { + return edge_value(g, e); + }); + + std::vector weights; + for (auto [e, w] : ilist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{1.5, 2.5}); + } + + SECTION("single edge vertex") { + auto v1 = vertex_t{1}; + auto ilist = incidence(g, v1); + + REQUIRE(ilist.size() == 1); + + auto [e] = *ilist.begin(); + REQUIRE(target_id(g, e) == 2); + REQUIRE(edge_value(g, e) == 3.5); + } +} + +// ============================================================================= +// Test 15: Map Vertices + Map Edges (Fully Sparse Graph) +// ============================================================================= + +TEST_CASE("incidence - map vertices map edges", "[incidence][map][edge_map]") { + // Both vertices and edges in maps - fully sparse graph + using Graph = std::map>; + Graph g = { + {10, {{20, 1.0}, {30, 2.0}}}, // vertex 10 -> (20, 1.0), (30, 2.0) + {20, {{30, 3.0}}}, // vertex 20 -> (30, 3.0) + {30, {}} // vertex 30 -> no edges + }; + + SECTION("iteration") { + auto verts = vertices(g); + auto v10 = *verts.begin(); + REQUIRE(v10.vertex_id() == 10); + + auto ilist = incidence(g, v10); + + REQUIRE(ilist.size() == 2); + + std::vector targets; + for (auto [e] : ilist) { + targets.push_back(target_id(g, e)); + } + + REQUIRE(targets == std::vector{20, 30}); + } + + SECTION("with value function for edge weights") { + auto verts = vertices(g); + auto v10 = *verts.begin(); + + auto ilist = incidence(g, v10, [&g](auto e) { + return edge_value(g, e); + }); + + std::vector weights; + for (auto [e, w] : ilist) { + weights.push_back(w); + } + + REQUIRE(weights == std::vector{1.0, 2.0}); + } + + SECTION("source_id correct for sparse vertex") { + auto verts = vertices(g); + auto it = verts.begin(); + ++it; // vertex 20 + auto v20 = *it; + + for (auto [e] : incidence(g, v20)) { + REQUIRE(source_id(g, e) == 20); + REQUIRE(target_id(g, e) == 30); + } + } + + SECTION("all edges traversal") { + std::vector> all_edges; + + for (auto [v] : vertexlist(g)) { + for (auto [e, w] : incidence(g, v, [&g](auto e) { return edge_value(g, e); })) { + all_edges.emplace_back(source_id(g, e), target_id(g, e), w); + } + } + + REQUIRE(all_edges.size() == 3); + REQUIRE(std::get<0>(all_edges[0]) == 10); + REQUIRE(std::get<1>(all_edges[0]) == 20); + REQUIRE(std::get<2>(all_edges[0]) == 1.0); + } +} + +// ============================================================================= +// Test 16: undirected_adjacency_list - True Undirected Graph +// ============================================================================= +// Note: The undirected_adjacency_list is a true undirected graph where edges +// are not duplicated. Each edge is stored once but can be traversed from both +// endpoints. The incidence view tests verify this behavior. + +#include +#include + +TEST_CASE("incidence - undirected_adjacency_list basic", "[incidence][undirected]") { + using Graph = graph::container::undirected_adjacency_list; + Graph g; + + // Create vertices: 0, 1, 2, 3, 4 + g.create_vertex(100); // vertex 0, value=100 + g.create_vertex(200); // vertex 1, value=200 + g.create_vertex(300); // vertex 2, value=300 + g.create_vertex(400); // vertex 3, value=400 + g.create_vertex(500); // vertex 4, value=500 + + // Create edges from vertex 0 to multiple targets (star topology from 0) + // These are undirected edges - each creates one edge accessible from both ends + g.create_edge(0, 1, 10); // 0 -- 1, weight=10 + g.create_edge(0, 2, 20); // 0 -- 2, weight=20 + g.create_edge(0, 3, 30); // 0 -- 3, weight=30 + g.create_edge(0, 4, 40); // 0 -- 4, weight=40 + + // Additional edges to make vertex 2 a hub + g.create_edge(2, 3, 23); // 2 -- 3, weight=23 + g.create_edge(2, 4, 24); // 2 -- 4, weight=24 + + SECTION("vertex 0 has 4 incident edges") { + auto verts = vertices(g); + auto v0 = *verts.begin(); + + auto inc = incidence(g, v0); + REQUIRE(inc.size() == 4); + + // Collect all target IDs + std::set targets; + for (auto [e] : inc) { + targets.insert(target_id(g, e)); + } + + // Should have edges to 1, 2, 3, 4 + REQUIRE(targets.size() == 4); + REQUIRE(targets.count(1) == 1); + REQUIRE(targets.count(2) == 1); + REQUIRE(targets.count(3) == 1); + REQUIRE(targets.count(4) == 1); + } + + SECTION("vertex 1 has 1 incident edge (back to 0)") { + auto v1_it = find_vertex(g, 1u); + auto v1 = *v1_it; + + auto inc = incidence(g, v1); + REQUIRE(inc.size() == 1); + + auto [e] = *inc.begin(); + REQUIRE(target_id(g, e) == 0); // Points back to vertex 0 + } + + SECTION("vertex 2 has 3 incident edges") { + auto v2_it = find_vertex(g, 2u); + auto v2 = *v2_it; + + auto inc = incidence(g, v2); + REQUIRE(inc.size() == 3); + + std::set targets; + for (auto [e] : inc) { + targets.insert(target_id(g, e)); + } + + // Should have edges to 0, 3, 4 + REQUIRE(targets.count(0) == 1); + REQUIRE(targets.count(3) == 1); + REQUIRE(targets.count(4) == 1); + } + + SECTION("iterate with value function - edge weights") { + auto v0_it = find_vertex(g, 0u); + auto v0 = *v0_it; + + auto inc = incidence(g, v0, [&g](auto e) { return edge_value(g, e); }); + + std::vector weights; + std::vector targets; + for (auto [e, w] : inc) { + targets.push_back(target_id(g, e)); + weights.push_back(w); + } + + REQUIRE(weights.size() == 4); + + // Verify each target has the correct weight + for (size_t i = 0; i < targets.size(); ++i) { + // Weight should be target * 10 + REQUIRE(weights[i] == static_cast(targets[i] * 10)); + } + } + + SECTION("source_id is consistent for all edges from a vertex") { + auto v2_it = find_vertex(g, 2u); + auto v2 = *v2_it; + + for (auto [e] : incidence(g, v2)) { + REQUIRE(source_id(g, e) == 2); + } + } +} + +TEST_CASE("incidence - undirected_adjacency_list iteration order", "[incidence][undirected]") { + using Graph = graph::container::undirected_adjacency_list; + Graph g; + + // Create a simple triangle: 0 -- 1 -- 2 -- 0 + g.create_vertex(0); + g.create_vertex(1); + g.create_vertex(2); + + g.create_edge(0, 1, 1); + g.create_edge(1, 2, 2); + g.create_edge(2, 0, 3); + + SECTION("each vertex has exactly 2 incident edges") { + for (auto [v] : vertexlist(g)) { + auto inc = incidence(g, v); + REQUIRE(inc.size() == 2); + } + } + + SECTION("full graph traversal - each edge visited twice (once per direction)") { + std::vector> all_edges; + + for (auto [v] : vertexlist(g)) { + for (auto [e] : incidence(g, v)) { + all_edges.emplace_back(source_id(g, e), target_id(g, e)); + } + } + + // Triangle has 3 edges, each visited from both directions = 6 entries + REQUIRE(all_edges.size() == 6); + + // Verify we see each edge from both directions + std::set> edge_set(all_edges.begin(), all_edges.end()); + REQUIRE(edge_set.count({0, 1}) == 1); + REQUIRE(edge_set.count({1, 0}) == 1); + REQUIRE(edge_set.count({1, 2}) == 1); + REQUIRE(edge_set.count({2, 1}) == 1); + REQUIRE(edge_set.count({2, 0}) == 1); + REQUIRE(edge_set.count({0, 2}) == 1); + } +} + +TEST_CASE("incidence - undirected_adjacency_list range algorithms", "[incidence][undirected][algorithms]") { + using Graph = graph::container::undirected_adjacency_list; + Graph g; + + // Create vertices + for (int i = 0; i < 5; ++i) { + g.create_vertex(i * 100); + } + + // Create a hub at vertex 0 with many edges + g.create_edge(0, 1, 10); + g.create_edge(0, 2, 20); + g.create_edge(0, 3, 30); + g.create_edge(0, 4, 40); + + auto v0_it = find_vertex(g, 0u); + auto v0 = *v0_it; + + SECTION("std::ranges::distance") { + auto inc = incidence(g, v0); + REQUIRE(std::ranges::distance(inc) == 4); + } + + SECTION("std::ranges::count_if - count edges with weight > 20") { + auto inc = incidence(g, v0, [&g](auto e) { return edge_value(g, e); }); + + auto count = std::ranges::count_if(inc, [](auto ei) { + return ei.value > 20; + }); + + REQUIRE(count == 2); // edges with weights 30 and 40 + } + + SECTION("std::ranges::for_each - sum weights") { + auto inc = incidence(g, v0, [&g](auto e) { return edge_value(g, e); }); + + int total_weight = 0; + std::ranges::for_each(inc, [&total_weight](auto ei) { + total_weight += ei.value; + }); + + REQUIRE(total_weight == 100); // 10 + 20 + 30 + 40 + } + + SECTION("range-based for with structured bindings") { + auto inc = incidence(g, v0, [&g](auto e) { + return std::to_string(edge_value(g, e)); + }); + + std::vector weight_strs; + for (auto [e, w_str] : inc) { + weight_strs.push_back(w_str); + } + + REQUIRE(weight_strs.size() == 4); + } +} + +TEST_CASE("incidence - undirected_adjacency_list empty and single edge", "[incidence][undirected][edge_cases]") { + using Graph = graph::container::undirected_adjacency_list; + + SECTION("vertex with no edges") { + Graph g; + g.create_vertex(0); + g.create_vertex(1); + // No edges created + + auto v0_it = find_vertex(g, 0u); + auto v0 = *v0_it; + + auto inc = incidence(g, v0); + REQUIRE(inc.size() == 0); + REQUIRE(inc.begin() == inc.end()); + } + + SECTION("single edge - both endpoints see it") { + Graph g; + g.create_vertex(0); + g.create_vertex(1); + g.create_edge(0, 1, 42); + + auto v0_it = find_vertex(g, 0u); + auto v0 = *v0_it; + auto v1_it = find_vertex(g, 1u); + auto v1 = *v1_it; + + // From vertex 0 + auto inc0 = incidence(g, v0); + REQUIRE(inc0.size() == 1); + auto [e0] = *inc0.begin(); + REQUIRE(source_id(g, e0) == 0); + REQUIRE(target_id(g, e0) == 1); + REQUIRE(edge_value(g, e0) == 42); + + // From vertex 1 + auto inc1 = incidence(g, v1); + REQUIRE(inc1.size() == 1); + auto [e1] = *inc1.begin(); + REQUIRE(source_id(g, e1) == 1); + REQUIRE(target_id(g, e1) == 0); + REQUIRE(edge_value(g, e1) == 42); // Same edge, same weight + } +} + +TEST_CASE("incidence - undirected_adjacency_list with vertex_id convenience", "[incidence][undirected][uid]") { + using Graph = graph::container::undirected_adjacency_list; + Graph g; + + // Create vertices and edges + g.create_vertex(100); + g.create_vertex(200); + g.create_vertex(300); + g.create_edge(0, 1, 10); + g.create_edge(0, 2, 20); + g.create_edge(1, 2, 12); + + SECTION("incidence(g, uid) - basic iteration") { + // Use vertex_id directly instead of vertex descriptor + auto inc = incidence(g, 0u); + REQUIRE(inc.size() == 2); + + std::set targets; + for (auto [e] : inc) { + targets.insert(target_id(g, e)); + } + REQUIRE(targets.count(1) == 1); + REQUIRE(targets.count(2) == 1); + } + + SECTION("incidence(g, uid, evf) - with value function") { + auto inc = incidence(g, 0u, [&g](auto e) { return edge_value(g, e); }); + + std::vector weights; + for (auto [e, w] : inc) { + (void)e; + weights.push_back(w); + } + + REQUIRE(weights.size() == 2); + // Weights should be 10 and 20 (order may vary) + std::sort(weights.begin(), weights.end()); + REQUIRE(weights[0] == 10); + REQUIRE(weights[1] == 20); + } + + SECTION("incidence(g, uid) from different vertices") { + // From vertex 1 + auto inc1 = incidence(g, 1u); + REQUIRE(inc1.size() == 2); // edges to 0 and 2 + + // From vertex 2 + auto inc2 = incidence(g, 2u); + REQUIRE(inc2.size() == 2); // edges to 0 and 1 + } +} + +// ============================================================================= +// Test 21: undirected_adjacency_list - Dense Graph with 7+ Edges Per Vertex +// ============================================================================= +// This test creates a dense graph where each vertex has at least 7 incident edges. +// Edge targets are specified in non-sequential order to verify that the +// undirected structure correctly handles arbitrary connectivity patterns. + +TEST_CASE("incidence - undirected_adjacency_list dense graph", "[incidence][undirected][dense]") { + using Graph = graph::container::undirected_adjacency_list; + Graph g; + + // Create 10 vertices (0-9) with values + for (int i = 0; i < 10; ++i) { + g.create_vertex(i * 100); + } + + // Create edges from vertex 0 to 7 other vertices in NON-SEQUENTIAL order + // Targets: 7, 2, 9, 4, 1, 6, 3 (deliberately unordered) + g.create_edge(0, 7, 107); + g.create_edge(0, 2, 102); + g.create_edge(0, 9, 109); + g.create_edge(0, 4, 104); + g.create_edge(0, 1, 101); + g.create_edge(0, 6, 106); + g.create_edge(0, 3, 103); + + // Create edges from vertex 5 to 8 other vertices in NON-SEQUENTIAL order + // Targets: 9, 1, 8, 3, 6, 0, 4, 2 (deliberately unordered) + g.create_edge(5, 9, 159); + g.create_edge(5, 1, 151); + g.create_edge(5, 8, 158); + g.create_edge(5, 3, 153); + g.create_edge(5, 6, 156); + g.create_edge(5, 0, 150); + g.create_edge(5, 4, 154); + g.create_edge(5, 2, 152); + + // Create additional edges to ensure vertex 1 has 7+ edges + // Already connected to: 0, 5 + // Add connections to: 8, 3, 9, 7, 4, 2 in non-sequential order + g.create_edge(1, 8, 118); + g.create_edge(1, 3, 113); + g.create_edge(1, 9, 119); + g.create_edge(1, 7, 117); + g.create_edge(1, 4, 114); + g.create_edge(1, 2, 112); + + // Create additional edges to ensure vertex 9 has 7+ edges + // Already connected to: 0, 5, 1 + // Add connections to: 2, 7, 4, 8, 6 in non-sequential order + g.create_edge(9, 2, 129); + g.create_edge(9, 7, 179); + g.create_edge(9, 4, 149); + g.create_edge(9, 8, 189); + g.create_edge(9, 6, 169); + + SECTION("vertex 0 has exactly 8 incident edges (7 original + 1 from vertex 5)") { + auto inc = incidence(g, 0u); + REQUIRE(inc.size() == 8); + + // Collect all targets and weights + std::map target_to_weight; + for (auto [e] : inc) { + target_to_weight[target_id(g, e)] = edge_value(g, e); + } + + // Verify all expected targets are present + REQUIRE(target_to_weight.size() == 8); + REQUIRE(target_to_weight.count(1) == 1); + REQUIRE(target_to_weight.count(2) == 1); + REQUIRE(target_to_weight.count(3) == 1); + REQUIRE(target_to_weight.count(4) == 1); + REQUIRE(target_to_weight.count(5) == 1); // from edge 5->0 + REQUIRE(target_to_weight.count(6) == 1); + REQUIRE(target_to_weight.count(7) == 1); + REQUIRE(target_to_weight.count(9) == 1); + + // Verify weights (edge values encode source*100 + target or min*10 + max pattern) + REQUIRE(target_to_weight[1] == 101); + REQUIRE(target_to_weight[2] == 102); + REQUIRE(target_to_weight[3] == 103); + REQUIRE(target_to_weight[4] == 104); + REQUIRE(target_to_weight[5] == 150); // edge created as 5->0 + REQUIRE(target_to_weight[6] == 106); + REQUIRE(target_to_weight[7] == 107); + REQUIRE(target_to_weight[9] == 109); + } + + SECTION("vertex 5 has exactly 8 incident edges") { + auto inc = incidence(g, 5u); + REQUIRE(inc.size() == 8); + + std::set targets; + for (auto [e] : inc) { + targets.insert(target_id(g, e)); + } + + REQUIRE(targets == std::set{0, 1, 2, 3, 4, 6, 8, 9}); + } + + SECTION("vertex 1 has exactly 8 incident edges") { + auto inc = incidence(g, 1u); + REQUIRE(inc.size() == 8); + + std::set targets; + for (auto [e] : inc) { + targets.insert(target_id(g, e)); + } + + REQUIRE(targets == std::set{0, 2, 3, 4, 5, 7, 8, 9}); + } + + SECTION("vertex 9 has exactly 8 incident edges") { + auto inc = incidence(g, 9u); + REQUIRE(inc.size() == 8); + + std::set targets; + for (auto [e] : inc) { + targets.insert(target_id(g, e)); + } + + REQUIRE(targets == std::set{0, 1, 2, 4, 5, 6, 7, 8}); + } + + SECTION("iterate all edges from vertex 0 with value function") { + auto inc = incidence(g, 0u, [&g](auto e) { return edge_value(g, e); }); + + int total_weight = 0; + std::vector> edges_found; + + for (auto [e, w] : inc) { + total_weight += w; + edges_found.emplace_back(target_id(g, e), w); + } + + REQUIRE(edges_found.size() == 8); + // Sum: 101+102+103+104+106+107+109+150 = 882 + REQUIRE(total_weight == 882); + } + + SECTION("iterate all edges from vertex 5 with value function") { + auto inc = incidence(g, 5u, [&g](auto e) { return edge_value(g, e); }); + + int total_weight = 0; + size_t count = 0; + + for (auto [e, w] : inc) { + total_weight += w; + ++count; + + // Verify source is always 5 + REQUIRE(source_id(g, e) == 5); + } + + REQUIRE(count == 8); + // Sum: 150+151+152+153+154+156+158+159 = 1233 + REQUIRE(total_weight == 1233); + } + + SECTION("source_id is consistent when iterating from each vertex") { + for (unsigned int uid = 0; uid < 10; ++uid) { + for (auto [e] : incidence(g, uid)) { + REQUIRE(source_id(g, e) == uid); + } + } + } + + SECTION("each edge is seen from both endpoints with same weight") { + // For each edge from vertex 0, verify it's visible from the other end + for (auto [e0] : incidence(g, 0u)) { + auto tid = target_id(g, e0); + auto weight = edge_value(g, e0); + + // Find the reverse edge from target back to 0 + bool found_reverse = false; + for (auto [e_rev] : incidence(g, tid)) { + if (target_id(g, e_rev) == 0) { + found_reverse = true; + REQUIRE(edge_value(g, e_rev) == weight); + REQUIRE(source_id(g, e_rev) == tid); + break; + } + } + REQUIRE(found_reverse); + } + } + + SECTION("full graph traversal - verify edge symmetry") { + // Count how many times each edge is seen (should be exactly 2) + std::map, int> edge_count; + + for (unsigned int uid = 0; uid < 10; ++uid) { + for (auto [e] : incidence(g, uid)) { + auto src = source_id(g, e); + auto tgt = target_id(g, e); + // Normalize to (min, max) for counting + auto key = std::make_pair(std::min(src, tgt), std::max(src, tgt)); + edge_count[key]++; + } + } + + // Each edge should be counted exactly twice + for (const auto& [edge_pair, count] : edge_count) { + REQUIRE(count == 2); + } + + // Total unique edges created: + // vertex 0: 7 edges (to 1,2,3,4,6,7,9) + // vertex 5: 8 edges (to 0,1,2,3,4,6,8,9) + // vertex 1: 6 additional (to 2,3,4,7,8,9 - already counted 0,5) + // vertex 9: 5 additional (to 2,4,6,7,8 - already counted 0,1,5) + // Total: 7 + 8 + 6 + 5 = 26 unique edges + REQUIRE(edge_count.size() == 26); + } + + SECTION("range algorithms on dense incidence list") { + auto inc = incidence(g, 0u, [&g](auto e) { return edge_value(g, e); }); + + // Count edges with weight > 105 + auto count_high = std::ranges::count_if(inc, [](auto ei) { + return ei.value > 105; + }); + REQUIRE(count_high == 4); // 106, 107, 109, 150 + + // Find max weight + auto max_it = std::ranges::max_element(inc, {}, [](auto ei) { + return ei.value; + }); + REQUIRE(max_it != std::ranges::end(inc)); + REQUIRE((*max_it).value == 150); + } +} diff --git a/tests/views/test_main.cpp b/tests/views/test_main.cpp new file mode 100644 index 0000000..2a31758 --- /dev/null +++ b/tests/views/test_main.cpp @@ -0,0 +1,5 @@ +#include + +int main(int argc, char* argv[]) { + return Catch::Session().run(argc, argv); +} diff --git a/tests/views/test_neighbor_info.cpp b/tests/views/test_neighbor_info.cpp new file mode 100644 index 0000000..a8b632d --- /dev/null +++ b/tests/views/test_neighbor_info.cpp @@ -0,0 +1,442 @@ +#include +#include +#include + +using namespace graph; + +// Mock types for testing +struct mock_vertex_descriptor { + int id; +}; + +struct mock_value { + double data; +}; + +TEST_CASE("neighbor_info: all 16 specializations compile", "[neighbor_info]") { + SECTION("VId, Sourced=true, V, VV all present") { + neighbor_info ni{1, 2, mock_vertex_descriptor{10}, mock_value{42.0}}; + REQUIRE(ni.source_id == 1); + REQUIRE(ni.target_id == 2); + REQUIRE(ni.target.id == 10); + REQUIRE(ni.value.data == 42.0); + } + + SECTION("VId, Sourced=true, V present; VV=void") { + neighbor_info ni{2, 3, mock_vertex_descriptor{20}}; + REQUIRE(ni.source_id == 2); + REQUIRE(ni.target_id == 3); + REQUIRE(ni.target.id == 20); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=true, VV present; V=void") { + neighbor_info ni{3, 4, mock_value{99.9}}; + REQUIRE(ni.source_id == 3); + REQUIRE(ni.target_id == 4); + REQUIRE(ni.value.data == 99.9); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=true; V=void, VV=void") { + neighbor_info ni{4, 5}; + REQUIRE(ni.source_id == 4); + REQUIRE(ni.target_id == 5); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=false, V, VV all present") { + neighbor_info ni{5, mock_vertex_descriptor{30}, mock_value{123.4}}; + REQUIRE(ni.target_id == 5); + REQUIRE(ni.target.id == 30); + REQUIRE(ni.value.data == 123.4); + } + + SECTION("VId, Sourced=false, V present; VV=void") { + neighbor_info ni{6, mock_vertex_descriptor{40}}; + REQUIRE(ni.target_id == 6); + REQUIRE(ni.target.id == 40); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=false, VV present; V=void") { + neighbor_info ni{7, mock_value{77.7}}; + REQUIRE(ni.target_id == 7); + REQUIRE(ni.value.data == 77.7); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, Sourced=false; V=void, VV=void") { + neighbor_info ni{8}; + REQUIRE(ni.target_id == 8); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true; V, VV present (descriptor-based)") { + neighbor_info ni{mock_vertex_descriptor{50}, mock_value{200.0}}; + REQUIRE(ni.vertex.id == 50); + REQUIRE(ni.value.data == 200.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true, VV=void; V present") { + neighbor_info ni{mock_vertex_descriptor{60}}; + REQUIRE(ni.vertex.id == 60); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true, V=void; VV present") { + neighbor_info ni{mock_value{300.0}}; + REQUIRE(ni.value.data == 300.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=true; V=void, VV=void (empty)") { + neighbor_info ni{}; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false; V, VV present (primary pattern)") { + neighbor_info ni{mock_vertex_descriptor{70}, mock_value{400.0}}; + REQUIRE(ni.vertex.id == 70); + REQUIRE(ni.value.data == 400.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false, VV=void; V present") { + neighbor_info ni{mock_vertex_descriptor{80}}; + REQUIRE(ni.vertex.id == 80); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false, V=void; VV present") { + neighbor_info ni{mock_value{500.0}}; + REQUIRE(ni.value.data == 500.0); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, Sourced=false; V=void, VV=void (empty)") { + neighbor_info ni{}; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("neighbor_info: structured bindings work correctly", "[neighbor_info]") { + SECTION("Sourced=true, all four members") { + neighbor_info ni{1, 2, mock_vertex_descriptor{10}, mock_value{42.0}}; + auto [sid, tid, t, val] = ni; + REQUIRE(sid == 1); + REQUIRE(tid == 2); + REQUIRE(t.id == 10); + REQUIRE(val.data == 42.0); + } + + SECTION("Sourced=false, three members") { + neighbor_info ni{5, mock_vertex_descriptor{30}, mock_value{123.4}}; + auto [tid, t, val] = ni; + REQUIRE(tid == 5); + REQUIRE(t.id == 30); + REQUIRE(val.data == 123.4); + } + + SECTION("Three members: source_id, target_id and target") { + neighbor_info ni{2, 3, mock_vertex_descriptor{20}}; + auto [sid, tid, t] = ni; + REQUIRE(sid == 2); + REQUIRE(tid == 3); + REQUIRE(t.id == 20); + } + + SECTION("Two members: target_id and value") { + neighbor_info ni{7, mock_value{77.7}}; + auto [tid, val] = ni; + REQUIRE(tid == 7); + REQUIRE(val.data == 77.7); + } + + SECTION("Two members: source_id and target_id only") { + neighbor_info ni{4, 5}; + auto [sid, tid] = ni; + REQUIRE(sid == 4); + REQUIRE(tid == 5); + } + + SECTION("Primary pattern: vertex and value (descriptor-based)") { + neighbor_info ni{mock_vertex_descriptor{70}, mock_value{400.0}}; + auto [v, val] = ni; + REQUIRE(v.id == 70); + REQUIRE(val.data == 400.0); + } + + SECTION("Descriptor-based: vertex only") { + neighbor_info ni{mock_vertex_descriptor{60}}; + auto [v] = ni; + REQUIRE(v.id == 60); + } + + SECTION("Descriptor-based: value only") { + neighbor_info ni{mock_value{500.0}}; + auto [val] = ni; + REQUIRE(val.data == 500.0); + } +} + +TEST_CASE("neighbor_info: sizeof verifies physical absence of void members", "[neighbor_info]") { + SECTION("Full struct vs VId=void reduces size") { + using full_t = neighbor_info; + using no_id_t = neighbor_info; + + // No source_id/target_id members should be smaller or equal (padding may prevent strict reduction) + REQUIRE(sizeof(no_id_t) <= sizeof(full_t)); + // Should be at most the size of the two members plus padding + REQUIRE(sizeof(no_id_t) <= sizeof(mock_vertex_descriptor) + sizeof(mock_value) + 2*sizeof(int)); + } + + SECTION("IDs only struct (Sourced=true)") { + using ids_only_t = neighbor_info; + REQUIRE(sizeof(ids_only_t) == 2 * sizeof(int)); // source_id + target_id + } + + SECTION("target_id only struct (Sourced=false)") { + using id_only_t = neighbor_info; + REQUIRE(sizeof(id_only_t) == sizeof(size_t)); // Only target_id + } + + SECTION("Empty structs") { + using empty_sourced_t = neighbor_info; + using empty_unsourced_t = neighbor_info; + + // Empty struct has size 1 in C++ (must be distinct) + REQUIRE(sizeof(empty_sourced_t) >= 1); + REQUIRE(sizeof(empty_unsourced_t) >= 1); + } +} + +TEST_CASE("neighbor_info: Sourced parameter affects member presence", "[neighbor_info]") { + SECTION("Sourced=true has source_id and target_id") { + neighbor_info ni{42, 99}; + REQUIRE(ni.source_id == 42); + REQUIRE(ni.target_id == 99); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Sourced=false has only target_id") { + neighbor_info ni{99}; + REQUIRE(ni.target_id == 99); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("neighbor_info: copyable and movable", "[neighbor_info]") { + SECTION("Copy construction - Sourced=true") { + neighbor_info ni1{1, 2, mock_vertex_descriptor{10}, mock_value{42.0}}; + neighbor_info ni2 = ni1; + REQUIRE(ni2.source_id == ni1.source_id); + REQUIRE(ni2.target_id == ni1.target_id); + REQUIRE(ni2.target.id == ni1.target.id); + REQUIRE(ni2.value.data == ni1.value.data); + } + + SECTION("Move construction - Sourced=false") { + neighbor_info ni1{5, mock_vertex_descriptor{30}, mock_value{123.4}}; + neighbor_info ni2 = std::move(ni1); + REQUIRE(ni2.target_id == 5); + REQUIRE(ni2.target.id == 30); + REQUIRE(ni2.value.data == 123.4); + } +} + +TEST_CASE("neighbor_info: descriptor-based pattern primary use case", "[neighbor_info]") { + SECTION("Primary pattern: neighbor_info") { + // This is THE primary pattern for neighbor views as per Section 8.3 of view_strategy + neighbor_info ni{mock_vertex_descriptor{100}, 3.14}; + + auto [v, val] = ni; + REQUIRE(v.id == 100); + REQUIRE(val == 3.14); + + // Verify no id members present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Descriptor without value function") { + neighbor_info ni{mock_vertex_descriptor{200}}; + + auto [v] = ni; + REQUIRE(v.id == 200); + + // Verify only target member present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("Sourced pattern: neighbor_info") { + // Used when iterating from known source + neighbor_info ni{ + mock_vertex_descriptor{300}, + std::string("neighbor_data") + }; + + auto [v, val] = ni; + REQUIRE(v.id == 300); + REQUIRE(val == "neighbor_data"); + + // Verify no ID members present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("neighbor_info: external data pattern use case", "[neighbor_info]") { + SECTION("Sourced external data: source_id, target_id and value") { + neighbor_info ni{100, 200, 12.34}; + + auto [sid, tid, val] = ni; + REQUIRE(sid == 100); + REQUIRE(tid == 200); + REQUIRE(val == 12.34); + + // Verify source_id, target_id and value present, target absent + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Unsourced external data: target_id and value") { + neighbor_info ni{42, std::string("data")}; + + auto [tid, val] = ni; + REQUIRE(tid == 42); + REQUIRE(val == "data"); + + // Verify target_id and value present, source_id and target absent + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("ID with target descriptor (external construction)") { + neighbor_info ni{999, mock_vertex_descriptor{400}}; + + auto [tid, tgt] = ni; + REQUIRE(tid == 999); + REQUIRE(tgt.id == 400); + + // Both target_id and target present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("neighbor_info: type traits are correct", "[neighbor_info]") { + SECTION("All type aliases match - Sourced=true") { + using ni_t = neighbor_info; + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("All type aliases match - Sourced=false") { + using ni_t = neighbor_info; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Void type aliases when void") { + using ni_t = neighbor_info; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("neighbor_info: copyable_neighbor_t alias works", "[neighbor_info]") { + SECTION("copyable_neighbor_t alias (Sourced=true)") { + using alias_t = copyable_neighbor_t; + using explicit_t = neighbor_info; + + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Alias used for sourced external data") { + copyable_neighbor_t cn{99, 100, 3.14}; + auto [sid, tid, val] = cn; + REQUIRE(sid == 99); + REQUIRE(tid == 100); + REQUIRE(val == 3.14); + } +} + +TEST_CASE("neighbor_info: relationship to view_strategy.md Section 8.3", "[neighbor_info]") { + SECTION("Section 8.3 specifies: neighbor_info") { + // Verify this is the exact pattern described in the strategy document + using strategy_pattern = neighbor_info; + + strategy_pattern ni{mock_vertex_descriptor{42}, 3.14159}; + + // Should yield {vertex descriptor, value} as per Section 8.3 + auto [v, val] = ni; + REQUIRE(v.id == 42); + REQUIRE(val == 3.14159); + + // The descriptor contains the vertex ID, so VId=void + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + + // Must have target member (the descriptor) + STATIC_REQUIRE(!std::is_void_v); + + // May or may not have value (VV can be void if no value function) + // In this case we have a value + STATIC_REQUIRE(!std::is_void_v); + } + + SECTION("Without value function: neighbor_info") { + using no_value_pattern = neighbor_info; + + no_value_pattern ni{mock_vertex_descriptor{99}}; + + // Should yield {vertex descriptor} only + auto [v] = ni; + REQUIRE(v.id == 99); + + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(!std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } +} diff --git a/tests/views/test_neighbors.cpp b/tests/views/test_neighbors.cpp new file mode 100644 index 0000000..af37b66 --- /dev/null +++ b/tests/views/test_neighbors.cpp @@ -0,0 +1,672 @@ +/** + * @file test_neighbors.cpp + * @brief Comprehensive tests for neighbors view + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +// ============================================================================= +// Test 1: Vertex with No Neighbors +// ============================================================================= + +TEST_CASE("neighbors - vertex with no neighbors", "[neighbors][empty]") { + using Graph = std::vector>; + Graph g = { + {}, // vertex 0 - no edges + {0}, // vertex 1 - edge to 0 + {0, 1} // vertex 2 - edges to 0 and 1 + }; + + SECTION("no value function - empty iteration") { + auto v0 = vertex_t{0}; // vertex 0 has no neighbors + auto nlist = neighbors(g, v0); + + REQUIRE(nlist.begin() == nlist.end()); + REQUIRE(nlist.size() == 0); + } + + SECTION("with value function - empty iteration") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0, [](auto /*v*/) { return 42; }); + + REQUIRE(nlist.begin() == nlist.end()); + } +} + +// ============================================================================= +// Test 2: Single Neighbor +// ============================================================================= + +TEST_CASE("neighbors - vertex with single neighbor", "[neighbors][single]") { + using Graph = std::vector>; + Graph g = { + {1}, // vertex 0 -> neighbor 1 + {} // vertex 1 - no neighbors + }; + + SECTION("no value function") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0); + + REQUIRE(nlist.size() == 1); + + auto it = nlist.begin(); + REQUIRE(it != nlist.end()); + + auto ni = *it; + // neighbor_info, void> has 'vertex' member + REQUIRE(ni.vertex.vertex_id() == 1); + + ++it; + REQUIRE(it == nlist.end()); + } + + SECTION("with value function") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0, [](auto v) { + return v.vertex_id() * 10; + }); + + REQUIRE(nlist.size() == 1); + + auto ni = *nlist.begin(); + REQUIRE(ni.vertex.vertex_id() == 1); + REQUIRE(ni.value == 10); // vertex_id(1) * 10 + } +} + +// ============================================================================= +// Test 3: Multiple Neighbors +// ============================================================================= + +TEST_CASE("neighbors - vertex with multiple neighbors", "[neighbors][multiple]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // vertex 0 -> neighbors 1, 2, 3 + {2, 3}, // vertex 1 -> neighbors 2, 3 + {3}, // vertex 2 -> neighbor 3 + {} // vertex 3 - no neighbors + }; + + SECTION("no value function - iteration") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0); + + REQUIRE(nlist.size() == 3); + + std::vector neighbor_ids; + for (auto ni : nlist) { + neighbor_ids.push_back(ni.vertex.vertex_id()); + } + + REQUIRE(neighbor_ids == std::vector{1, 2, 3}); + } + + SECTION("with value function") { + auto v1 = vertex_t{1}; + auto nlist = neighbors(g, v1, [](auto v) { + return static_cast(v.vertex_id() * 100); + }); + + std::vector values; + for (auto ni : nlist) { + values.push_back(ni.value); + } + + REQUIRE(values == std::vector{200, 300}); + } + + SECTION("structured binding - no value function") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0); + + std::vector neighbor_ids; + for (auto [v] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + } + REQUIRE(neighbor_ids == std::vector{1, 2, 3}); + } + + SECTION("structured binding - with value function") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0, [](auto v) { + return v.vertex_id() + 100; + }); + + std::vector neighbor_ids; + std::vector values; + for (auto [v, val] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + values.push_back(val); + } + + REQUIRE(neighbor_ids == std::vector{1, 2, 3}); + REQUIRE(values == std::vector{101, 102, 103}); + } +} + +// ============================================================================= +// Test 4: Value Function Types +// ============================================================================= + +TEST_CASE("neighbors - value function types", "[neighbors][vvf]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // vertex 0 -> neighbors 1, 2 + {}, + {} + }; + auto v0 = vertex_t{0}; + + SECTION("returning string") { + auto nlist = neighbors(g, v0, [](auto v) { + return "neighbor_" + std::to_string(v.vertex_id()); + }); + + std::vector names; + for (auto [v, name] : nlist) { + names.push_back(name); + } + + REQUIRE(names == std::vector{"neighbor_1", "neighbor_2"}); + } + + SECTION("returning double") { + auto nlist = neighbors(g, v0, [](auto v) { + return static_cast(v.vertex_id()) * 1.5; + }); + + std::vector values; + for (auto [v, val] : nlist) { + values.push_back(val); + } + + REQUIRE(values[0] == 1.5); + REQUIRE(values[1] == 3.0); + } + + SECTION("capturing lambda") { + std::size_t multiplier = 7; + auto nlist = neighbors(g, v0, [multiplier](auto v) { + return static_cast(v.vertex_id() * multiplier); + }); + + std::vector values; + for (auto [v, val] : nlist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{7, 14}); + } +} + +// ============================================================================= +// Test 5: Vertex Descriptor Access +// ============================================================================= + +TEST_CASE("neighbors - vertex descriptor access", "[neighbors][descriptor]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3}, // vertex 0 -> neighbors 1, 2, 3 + {}, + {}, + {} + }; + auto v0 = vertex_t{0}; + + SECTION("vertex_id access") { + auto nlist = neighbors(g, v0); + + std::vector ids; + for (auto [v] : nlist) { + ids.push_back(v.vertex_id()); + } + + REQUIRE(ids == std::vector{1, 2, 3}); + } + + SECTION("vertex descriptor type") { + auto nlist = neighbors(g, v0); + + for (auto [v] : nlist) { + // v should be a vertex descriptor + STATIC_REQUIRE(std::is_same_v>); + (void)v; // Suppress unused warning + } + } +} + +// ============================================================================= +// Test 6: Weighted Graph (Pair Edges) +// ============================================================================= + +TEST_CASE("neighbors - weighted graph", "[neighbors][weighted]") { + // Graph with weighted edges: vector>> + using Graph = std::vector>>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 -> (1, 1.5), (2, 2.5) + {{2, 3.5}}, // vertex 1 -> (2, 3.5) + {} + }; + + SECTION("no value function - neighbor iteration") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0); + + REQUIRE(nlist.size() == 2); + + std::vector neighbor_ids; + for (auto [v] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + } + + REQUIRE(neighbor_ids == std::vector{1, 2}); + } + + SECTION("value function accessing neighbor properties") { + auto v0 = vertex_t{0}; + // Value function that just returns neighbor ID squared + auto nlist = neighbors(g, v0, [](auto v) { + return v.vertex_id() * v.vertex_id(); + }); + + std::vector values; + for (auto [v, val] : nlist) { + values.push_back(val); + } + + REQUIRE(values[0] == 1); // 1^2 + REQUIRE(values[1] == 4); // 2^2 + } +} + +// ============================================================================= +// Test 7: Range Concepts +// ============================================================================= + +TEST_CASE("neighbors - range concepts", "[neighbors][concepts]") { + using Graph = std::vector>; + Graph g = {{1, 2}, {}, {}}; + auto v0 = vertex_t{0}; + + SECTION("no value function") { + auto nlist = neighbors(g, v0); + + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::sized_range); + STATIC_REQUIRE(std::ranges::view); + } + + SECTION("with value function") { + auto nlist = neighbors(g, v0, [](auto v) { return v.vertex_id(); }); + + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::sized_range); + } +} + +// ============================================================================= +// Test 8: Iterator Properties +// ============================================================================= + +TEST_CASE("neighbors - iterator properties", "[neighbors][iterator]") { + using Graph = std::vector>; + Graph g = {{1, 2, 3}, {}, {}, {}}; + auto v0 = vertex_t{0}; + + SECTION("pre-increment returns reference") { + auto nlist = neighbors(g, v0); + auto it = nlist.begin(); + auto& ref = ++it; + REQUIRE(&ref == &it); + } + + SECTION("post-increment returns copy") { + auto nlist = neighbors(g, v0); + auto it = nlist.begin(); + auto copy = it++; + REQUIRE(copy != it); + } + + SECTION("equality comparison") { + auto nlist = neighbors(g, v0); + auto it1 = nlist.begin(); + auto it2 = nlist.begin(); + REQUIRE(it1 == it2); + ++it1; + REQUIRE(it1 != it2); + } +} + +// ============================================================================= +// Test 9: neighbor_info Type Verification +// ============================================================================= + +TEST_CASE("neighbors - neighbor_info type verification", "[neighbors][types]") { + using Graph = std::vector>; + using VertexType = vertex_t; + + SECTION("no value function - neighbor_info") { + using ViewType = neighbors_view; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + + // Verify neighbor_info members + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("with value function - neighbor_info") { + using VVF = int(*)(VertexType); + using ViewType = neighbors_view; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_same_v>); + + // Verify neighbor_info members + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +// ============================================================================= +// Test 10: std::ranges Algorithms +// ============================================================================= + +TEST_CASE("neighbors - std::ranges algorithms", "[neighbors][algorithms]") { + using Graph = std::vector>; + Graph g = { + {1, 2, 3, 4, 5}, // vertex 0 -> neighbors 1-5 + {}, + {}, + {}, + {}, + {} + }; + auto v0 = vertex_t{0}; + + SECTION("std::ranges::distance") { + auto nlist = neighbors(g, v0); + REQUIRE(std::ranges::distance(nlist) == 5); + } + + SECTION("std::ranges::count_if") { + auto nlist = neighbors(g, v0); + auto count = std::ranges::count_if(nlist, [](auto ni) { + return ni.vertex.vertex_id() > 2; + }); + REQUIRE(count == 3); // neighbors 3, 4, 5 + } +} + +// ============================================================================= +// Test 11: Deque-based Graph +// ============================================================================= + +TEST_CASE("neighbors - deque-based graph", "[neighbors][deque]") { + using Graph = std::deque>; + Graph g = { + {1, 2}, + {2}, + {} + }; + + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0); + + std::vector neighbor_ids; + for (auto [v] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + } + + REQUIRE(neighbor_ids == std::vector{1, 2}); +} + +// ============================================================================= +// Test 12: All Vertices Iteration (vertexlist + neighbors) +// ============================================================================= + +TEST_CASE("neighbors - iterating all vertices", "[neighbors][all]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // vertex 0 -> 1, 2 + {2}, // vertex 1 -> 2 + {} // vertex 2 -> no neighbors + }; + + // Collect all neighbor relationships + std::vector> all_neighbors; + + for (auto [v] : vertexlist(g)) { + auto source_id = v.vertex_id(); + for (auto [neighbor] : neighbors(g, v)) { + all_neighbors.emplace_back(source_id, neighbor.vertex_id()); + } + } + + REQUIRE(all_neighbors.size() == 3); + REQUIRE(all_neighbors[0] == std::pair{0, 1}); + REQUIRE(all_neighbors[1] == std::pair{0, 2}); + REQUIRE(all_neighbors[2] == std::pair{1, 2}); +} + +// ============================================================================= +// Test 13: Map-Based Vertex Container (Sparse Vertex IDs) +// ============================================================================= + +TEST_CASE("neighbors - map vertices vector edges", "[neighbors][map]") { + // Map-based graphs have sparse, non-contiguous vertex IDs + using Graph = std::map>; + Graph g = { + {100, {200, 300}}, // vertex 100 -> neighbors 200, 300 + {200, {300}}, // vertex 200 -> neighbor 300 + {300, {}} // vertex 300 -> no neighbors + }; + + SECTION("iteration over neighbors from sparse vertex") { + // Get vertex 100 + auto verts = vertices(g); + auto v100 = *verts.begin(); + REQUIRE(v100.vertex_id() == 100); + + auto nlist = neighbors(g, v100); + + REQUIRE(nlist.size() == 2); + + std::vector neighbor_ids; + for (auto [v] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + } + + REQUIRE(neighbor_ids == std::vector{200, 300}); + } + + SECTION("empty neighbor list") { + auto verts = vertices(g); + auto it = verts.begin(); + std::advance(it, 2); // Get vertex 300 + auto v300 = *it; + REQUIRE(v300.vertex_id() == 300); + + auto nlist = neighbors(g, v300); + REQUIRE(nlist.size() == 0); + REQUIRE(nlist.begin() == nlist.end()); + } + + SECTION("with value function") { + auto verts = vertices(g); + auto v100 = *verts.begin(); + + auto nlist = neighbors(g, v100, [](auto v) { + return v.vertex_id() - 100; // Offset from base + }); + + std::vector offsets; + for (auto [v, offset] : nlist) { + offsets.push_back(offset); + } + + REQUIRE(offsets == std::vector{100, 200}); // 200-100=100, 300-100=200 + } + + SECTION("iterate all vertices and neighbors") { + std::vector> all_neighbors; + + for (auto [v] : vertexlist(g)) { + auto source_id = v.vertex_id(); + for (auto [neighbor] : neighbors(g, v)) { + all_neighbors.emplace_back(source_id, neighbor.vertex_id()); + } + } + + REQUIRE(all_neighbors.size() == 3); + REQUIRE(all_neighbors[0] == std::pair{100, 200}); + REQUIRE(all_neighbors[1] == std::pair{100, 300}); + REQUIRE(all_neighbors[2] == std::pair{200, 300}); + } +} + +// ============================================================================= +// Test 14: Map-Based Edge Container (Sorted Edges) +// ============================================================================= + +TEST_CASE("neighbors - vector vertices map edges", "[neighbors][edge_map]") { + // Edges stored in map (sorted by target, with edge values) + using Graph = std::vector>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 -> (1, 1.5), (2, 2.5) + {{2, 3.5}}, // vertex 1 -> (2, 3.5) + {} // vertex 2 -> no neighbors + }; + + SECTION("iteration") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0); + + REQUIRE(nlist.size() == 2); + + std::vector neighbor_ids; + for (auto [v] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + } + + // Map edges are sorted by target_id (key) + REQUIRE(neighbor_ids == std::vector{1, 2}); + } + + SECTION("with value function") { + auto v0 = vertex_t{0}; + auto nlist = neighbors(g, v0, [](auto v) { + return v.vertex_id() * 10; + }); + + std::vector values; + for (auto [v, val] : nlist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{10, 20}); + } + + SECTION("single neighbor vertex") { + auto v1 = vertex_t{1}; + auto nlist = neighbors(g, v1); + + REQUIRE(nlist.size() == 1); + + auto [v] = *nlist.begin(); + REQUIRE(v.vertex_id() == 2); + } +} + +// ============================================================================= +// Test 15: Map Vertices + Map Edges (Fully Sparse Graph) +// ============================================================================= + +TEST_CASE("neighbors - map vertices map edges", "[neighbors][map][edge_map]") { + // Both vertices and edges in maps - fully sparse graph + using Graph = std::map>; + Graph g = { + {10, {{20, 1.0}, {30, 2.0}}}, // vertex 10 -> (20, 1.0), (30, 2.0) + {20, {{30, 3.0}}}, // vertex 20 -> (30, 3.0) + {30, {}} // vertex 30 -> no neighbors + }; + + SECTION("iteration") { + auto verts = vertices(g); + auto v10 = *verts.begin(); + REQUIRE(v10.vertex_id() == 10); + + auto nlist = neighbors(g, v10); + + REQUIRE(nlist.size() == 2); + + std::vector neighbor_ids; + for (auto [v] : nlist) { + neighbor_ids.push_back(v.vertex_id()); + } + + REQUIRE(neighbor_ids == std::vector{20, 30}); + } + + SECTION("with value function") { + auto verts = vertices(g); + auto v10 = *verts.begin(); + + auto nlist = neighbors(g, v10, [](auto v) { + return v.vertex_id() * 2; + }); + + std::vector values; + for (auto [v, val] : nlist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{40, 60}); + } + + SECTION("all neighbors traversal") { + std::vector> all_neighbors; + + for (auto [v] : vertexlist(g)) { + auto source_id = v.vertex_id(); + for (auto [neighbor] : neighbors(g, v)) { + all_neighbors.emplace_back(source_id, neighbor.vertex_id()); + } + } + + REQUIRE(all_neighbors.size() == 3); + REQUIRE(all_neighbors[0] == std::pair{10, 20}); + REQUIRE(all_neighbors[1] == std::pair{10, 30}); + REQUIRE(all_neighbors[2] == std::pair{20, 30}); + } + + SECTION("neighbor descriptor type correct") { + auto verts = vertices(g); + auto v10 = *verts.begin(); + + for (auto [v] : neighbors(g, v10)) { + // v should be a vertex_t + STATIC_REQUIRE(std::is_same_v>); + (void)v; // Suppress unused warning + } + } +} diff --git a/tests/views/test_search_base.cpp b/tests/views/test_search_base.cpp new file mode 100644 index 0000000..0e8fdb2 --- /dev/null +++ b/tests/views/test_search_base.cpp @@ -0,0 +1,168 @@ +#include +#include + +using namespace graph::views; + +TEST_CASE("cancel_search enum values", "[views][search_base]") { + SECTION("enum has correct values") { + auto continue_val = cancel_search::continue_search; + auto branch_val = cancel_search::cancel_branch; + auto all_val = cancel_search::cancel_all; + + // Ensure they're distinct + REQUIRE(continue_val != branch_val); + REQUIRE(continue_val != all_val); + REQUIRE(branch_val != all_val); + } + + SECTION("can assign and compare") { + cancel_search cs = cancel_search::continue_search; + REQUIRE(cs == cancel_search::continue_search); + + cs = cancel_search::cancel_branch; + REQUIRE(cs == cancel_search::cancel_branch); + + cs = cancel_search::cancel_all; + REQUIRE(cs == cancel_search::cancel_all); + } +} + +TEST_CASE("visited_tracker basic functionality", "[views][search_base]") { + SECTION("construction with size") { + visited_tracker tracker(10); + REQUIRE(tracker.size() == 10); + } + + SECTION("initial state is unvisited") { + visited_tracker tracker(5); + for (std::size_t i = 0; i < 5; ++i) { + REQUIRE_FALSE(tracker.is_visited(i)); + } + } + + SECTION("mark_visited and is_visited") { + visited_tracker tracker(10); + + REQUIRE_FALSE(tracker.is_visited(3)); + tracker.mark_visited(3); + REQUIRE(tracker.is_visited(3)); + + // Other vertices remain unvisited + REQUIRE_FALSE(tracker.is_visited(2)); + REQUIRE_FALSE(tracker.is_visited(4)); + } + + SECTION("multiple visits") { + visited_tracker tracker(10); + + tracker.mark_visited(0); + tracker.mark_visited(5); + tracker.mark_visited(9); + + REQUIRE(tracker.is_visited(0)); + REQUIRE(tracker.is_visited(5)); + REQUIRE(tracker.is_visited(9)); + REQUIRE_FALSE(tracker.is_visited(1)); + REQUIRE_FALSE(tracker.is_visited(4)); + REQUIRE_FALSE(tracker.is_visited(8)); + } +} + +TEST_CASE("visited_tracker reset", "[views][search_base]") { + visited_tracker tracker(10); + + // Mark several as visited + tracker.mark_visited(2); + tracker.mark_visited(5); + tracker.mark_visited(7); + + REQUIRE(tracker.is_visited(2)); + REQUIRE(tracker.is_visited(5)); + REQUIRE(tracker.is_visited(7)); + + // Reset + tracker.reset(); + + // All should be unvisited now + for (std::size_t i = 0; i < 10; ++i) { + REQUIRE_FALSE(tracker.is_visited(i)); + } +} + +TEST_CASE("visited_tracker with different VId types", "[views][search_base]") { + SECTION("size_t") { + visited_tracker tracker(5); + tracker.mark_visited(std::size_t{2}); + REQUIRE(tracker.is_visited(std::size_t{2})); + } + + SECTION("int") { + visited_tracker tracker(5); + tracker.mark_visited(2); + REQUIRE(tracker.is_visited(2)); + } + + SECTION("unsigned int") { + visited_tracker tracker(5); + tracker.mark_visited(2u); + REQUIRE(tracker.is_visited(2u)); + } + + SECTION("uint32_t") { + visited_tracker tracker(5); + tracker.mark_visited(std::uint32_t{2}); + REQUIRE(tracker.is_visited(std::uint32_t{2})); + } +} + +TEST_CASE("visited_tracker edge cases", "[views][search_base]") { + SECTION("empty tracker") { + visited_tracker tracker(0); + REQUIRE(tracker.size() == 0); + } + + SECTION("single vertex") { + visited_tracker tracker(1); + REQUIRE(tracker.size() == 1); + REQUIRE_FALSE(tracker.is_visited(0)); + + tracker.mark_visited(0); + REQUIRE(tracker.is_visited(0)); + } + + SECTION("large graph") { + visited_tracker tracker(10000); + REQUIRE(tracker.size() == 10000); + + tracker.mark_visited(0); + tracker.mark_visited(5000); + tracker.mark_visited(9999); + + REQUIRE(tracker.is_visited(0)); + REQUIRE(tracker.is_visited(5000)); + REQUIRE(tracker.is_visited(9999)); + REQUIRE_FALSE(tracker.is_visited(1)); + REQUIRE_FALSE(tracker.is_visited(5001)); + } + + SECTION("visit all vertices") { + visited_tracker tracker(10); + for (std::size_t i = 0; i < 10; ++i) { + tracker.mark_visited(i); + } + + for (std::size_t i = 0; i < 10; ++i) { + REQUIRE(tracker.is_visited(i)); + } + } +} + +TEST_CASE("visited_tracker with custom allocator", "[views][search_base]") { + // Using default allocator explicitly + std::allocator alloc; + visited_tracker> tracker(5, alloc); + + REQUIRE(tracker.size() == 5); + tracker.mark_visited(2); + REQUIRE(tracker.is_visited(2)); +} diff --git a/tests/views/test_topological_sort.cpp b/tests/views/test_topological_sort.cpp new file mode 100644 index 0000000..abe6fce --- /dev/null +++ b/tests/views/test_topological_sort.cpp @@ -0,0 +1,900 @@ +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::adj_list; +using namespace graph::views; + +TEST_CASE("vertices_topological_sort - simple DAG", "[topo][vertices]") { + using Graph = std::vector>; + // DAG: 0 -> 1 -> 2 + Graph g = { + {1}, // 0 -> 1 + {2}, // 1 -> 2 + {} // 2 (sink) + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 3); + // 0 must come before 1, and 1 before 2 + auto pos_0 = std::find(order.begin(), order.end(), 0) - order.begin(); + auto pos_1 = std::find(order.begin(), order.end(), 1) - order.begin(); + auto pos_2 = std::find(order.begin(), order.end(), 2) - order.begin(); + REQUIRE(pos_0 < pos_1); + REQUIRE(pos_1 < pos_2); +} + +TEST_CASE("vertices_topological_sort - diamond DAG", "[topo][vertices]") { + using Graph = std::vector>; + // Diamond: 0 -> [1, 2], 1 -> 3, 2 -> 3 + Graph g = { + {1, 2}, // 0 -> 1, 2 + {3}, // 1 -> 3 + {3}, // 2 -> 3 + {} // 3 (sink) + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 4); + + // Verify topological constraints: each edge points forward + auto pos = [&](int vid) { + return std::find(order.begin(), order.end(), vid) - order.begin(); + }; + + // 0 must come before 1, 2, and 3 + REQUIRE(pos(0) < pos(1)); + REQUIRE(pos(0) < pos(2)); + REQUIRE(pos(0) < pos(3)); + + // 1 and 2 must both come before 3 + REQUIRE(pos(1) < pos(3)); + REQUIRE(pos(2) < pos(3)); +} + +TEST_CASE("vertices_topological_sort - structured binding [v]", "[topo][vertices]") { + using Graph = std::vector>; + Graph g = { + {1}, + {} + }; + + int count = 0; + for (auto [v] : vertices_topological_sort(g)) { + REQUIRE(vertex_id(g, v) < g.size()); + ++count; + } + + REQUIRE(count == 2); +} + +TEST_CASE("vertices_topological_sort - structured binding [v, val]", "[topo][vertices]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {}, + {} + }; + + auto topo = vertices_topological_sort(g, [&g](auto v) { + return static_cast(vertex_id(g, v)) * 10; + }); + + std::vector values; + for (auto [v, val] : topo) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + // Check that we got the expected values (in some order) + REQUIRE(std::find(values.begin(), values.end(), 0) != values.end()); + REQUIRE(std::find(values.begin(), values.end(), 10) != values.end()); + REQUIRE(std::find(values.begin(), values.end(), 20) != values.end()); +} + +TEST_CASE("vertices_topological_sort - value function receives descriptor", "[topo][vertices]") { + using Graph = std::vector>; + Graph g = { + {1}, + {2}, + {} + }; + + // Value function receives descriptor, not ID + auto topo = vertices_topological_sort(g, [&g](auto v) { + // Can use vertex_id to convert descriptor to ID + return static_cast(vertex_id(g, v)) * 100; + }); + + std::vector values; + for (auto [v, val] : topo) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + REQUIRE(std::find(values.begin(), values.end(), 0) != values.end()); + REQUIRE(std::find(values.begin(), values.end(), 100) != values.end()); + REQUIRE(std::find(values.begin(), values.end(), 200) != values.end()); +} + +TEST_CASE("vertices_topological_sort - complex DAG", "[topo][vertices]") { + using Graph = std::vector>; + // More complex DAG with multiple paths + // 0 + // / \ (backslash) + // 1 2 3 + // |X| | + // 4 5 6 + // \|/ + // 7 + Graph g = { + {1, 2, 3}, // 0 + {4, 5}, // 1 + {4, 5}, // 2 + {6}, // 3 + {7}, // 4 + {7}, // 5 + {7}, // 6 + {} // 7 (sink) + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 8); + + auto pos = [&](int vid) { + return std::find(order.begin(), order.end(), vid) - order.begin(); + }; + + // Verify all edges point forward in the ordering + REQUIRE(pos(0) < pos(1)); + REQUIRE(pos(0) < pos(2)); + REQUIRE(pos(0) < pos(3)); + REQUIRE(pos(1) < pos(4)); + REQUIRE(pos(1) < pos(5)); + REQUIRE(pos(2) < pos(4)); + REQUIRE(pos(2) < pos(5)); + REQUIRE(pos(3) < pos(6)); + REQUIRE(pos(4) < pos(7)); + REQUIRE(pos(5) < pos(7)); + REQUIRE(pos(6) < pos(7)); +} + +TEST_CASE("vertices_topological_sort - single vertex", "[topo][vertices]") { + using Graph = std::vector>; + Graph g = { + {} // 0 (isolated) + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 1); + REQUIRE(order[0] == 0); +} + +TEST_CASE("vertices_topological_sort - disconnected components", "[topo][vertices]") { + using Graph = std::vector>; + // Two separate chains: 0->1->2 and 3->4->5 + Graph g = { + {1}, // 0 + {2}, // 1 + {}, // 2 + {4}, // 3 + {5}, // 4 + {} // 5 + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 6); + + auto pos = [&](int vid) { + return std::find(order.begin(), order.end(), vid) - order.begin(); + }; + + // Within each component, order must be preserved + REQUIRE(pos(0) < pos(1)); + REQUIRE(pos(1) < pos(2)); + REQUIRE(pos(3) < pos(4)); + REQUIRE(pos(4) < pos(5)); +} + +TEST_CASE("vertices_topological_sort - all edges point forward", "[topo][vertices]") { + using Graph = std::vector>; + // Random DAG structure + Graph g = { + {2, 3}, // 0 + {3, 4}, // 1 + {5}, // 2 + {5, 6}, // 3 + {6}, // 4 + {7}, // 5 + {7}, // 6 + {} // 7 + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + // Build position map + std::vector position(g.size()); + for (std::size_t i = 0; i < order.size(); ++i) { + position[static_cast(order[i])] = i; + } + + // Verify ALL edges point forward + for (std::size_t u = 0; u < g.size(); ++u) { + for (int v_id : g[u]) { + REQUIRE(position[u] < position[static_cast(v_id)]); + } + } +} + +TEST_CASE("vertices_topological_sort - size accessor", "[topo][vertices]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, + {3}, + {3}, + {} + }; + + auto topo = vertices_topological_sort(g); + + REQUIRE(topo.size() == 4); + + int count = 0; + for (auto [v] : topo) { + ++count; + } + + REQUIRE(count == 4); + REQUIRE(topo.size() == 4); // Size remains constant +} + +TEST_CASE("vertices_topological_sort - empty graph", "[topo][vertices]") { + using Graph = std::vector>; + Graph g; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.empty()); +} + +TEST_CASE("vertices_topological_sort - linear chain", "[topo][vertices]") { + using Graph = std::vector>; + // 0 -> 1 -> 2 -> 3 -> 4 + Graph g = { + {1}, + {2}, + {3}, + {4}, + {} + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 5); + // Must be exactly [0, 1, 2, 3, 4] + for (std::size_t i = 0; i < order.size(); ++i) { + REQUIRE(order[i] == static_cast(i)); + } +} + +TEST_CASE("vertices_topological_sort - wide DAG", "[topo][vertices]") { + using Graph = std::vector>; + // Single source to many sinks + // 0 -> [1, 2, 3, 4, 5] + Graph g = { + {1, 2, 3, 4, 5}, + {}, + {}, + {}, + {}, + {} + }; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(static_cast(vertex_id(g, v))); + } + + REQUIRE(order.size() == 6); + REQUIRE(order[0] == 0); // Source must be first + + // All others come after 0 + for (std::size_t i = 1; i < order.size(); ++i) { + REQUIRE(order[i] > 0); + } +} + +//=========================================================================== +// edges_topological_sort tests +//=========================================================================== + +TEST_CASE("edges_topological_sort - simple DAG", "[topo][edges]") { + using Graph = std::vector>; + // 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + std::vector> edges; + for (auto [e] : edges_topological_sort(g)) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 2); + REQUIRE(edges[0] == std::make_pair(0, 1)); + REQUIRE(edges[1] == std::make_pair(1, 2)); + + // Verify sources follow topological order + for (std::size_t i = 1; i < edges.size(); ++i) { + REQUIRE(edges[i-1].first <= edges[i].first); + } +} + +TEST_CASE("edges_topological_sort - diamond DAG", "[topo][edges]") { + using Graph = std::vector>; + // 0 + // / \ + // 1 2 + // \ / + // 3 + Graph g = {{1, 2}, {3}, {3}, {}}; + + std::map> edge_map; + for (auto [e] : edges_topological_sort(g)) { + auto src = source_id(g, e); + auto tgt = target_id(g, e); + edge_map[src].insert(tgt); + } + + REQUIRE(edge_map.size() == 3); + REQUIRE(edge_map[0].size() == 2); + REQUIRE(edge_map[0].count(1) == 1); + REQUIRE(edge_map[0].count(2) == 1); + REQUIRE(edge_map[1].size() == 1); + REQUIRE(edge_map[1].count(3) == 1); + REQUIRE(edge_map[2].size() == 1); + REQUIRE(edge_map[2].count(3) == 1); +} + +TEST_CASE("edges_topological_sort - structured binding with value", "[topo][edges]") { + using Graph = std::vector>; + // 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + std::vector> edges_with_values; + for (auto [e, val] : edges_topological_sort(g, [](auto ed) { return 42; })) { + edges_with_values.emplace_back(source_id(g, e), target_id(g, e), val); + } + + REQUIRE(edges_with_values.size() == 2); + for (const auto& [src, tgt, val] : edges_with_values) { + REQUIRE(val == 42); + } +} + +TEST_CASE("edges_topological_sort - value function receives descriptor", "[topo][edges]") { + using Graph = std::vector>; + // 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + std::vector edge_ids; + for (auto [e, id] : edges_topological_sort(g, [&g](auto ed) { + return source_id(g, ed) * 10 + target_id(g, ed); + })) { + edge_ids.push_back(id); + } + + REQUIRE(edge_ids.size() == 2); + REQUIRE(edge_ids[0] == 1); // 0*10 + 1 + REQUIRE(edge_ids[1] == 12); // 1*10 + 2 +} + +TEST_CASE("edges_topological_sort - complex DAG", "[topo][edges]") { + using Graph = std::vector>; + // 0 + // / \ + // 1 2 + // |\ /| + // | X | + // |/ \| + // 3 4 + // \ / + // 5 + Graph g = { + {1, 2}, // 0 + {3, 4}, // 1 + {3, 4}, // 2 + {5}, // 3 + {5}, // 4 + {} // 5 + }; + + int edge_count = 0; + std::set> seen_edges; + std::map vertex_positions; // vertex_id -> position in topological order + + // First, get vertex positions from vertices_topological_sort + int pos = 0; + for (auto [v] : vertices_topological_sort(g)) { + vertex_positions[vertex_id(g, v)] = pos++; + } + + // Now verify edges follow topological order (sources before targets) + for (auto [e] : edges_topological_sort(g)) { + auto src = source_id(g, e); + auto tgt = target_id(g, e); + + seen_edges.emplace(src, tgt); + ++edge_count; + + // Verify source comes before target in topological order + REQUIRE(vertex_positions[src] < vertex_positions[tgt]); + } + + REQUIRE(edge_count == 8); + REQUIRE(seen_edges.size() == 8); +} + +TEST_CASE("edges_topological_sort - disconnected components", "[topo][edges]") { + using Graph = std::vector>; + // 0 -> 1 2 -> 3 + Graph g = {{1}, {}, {3}, {}}; + + std::set> edges; + for (auto [e] : edges_topological_sort(g)) { + edges.emplace(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 2); + REQUIRE(edges.count({0, 1}) == 1); + REQUIRE(edges.count({2, 3}) == 1); +} + +TEST_CASE("edges_topological_sort - empty graph", "[topo][edges]") { + using Graph = std::vector>; + Graph g; + + int count = 0; + for (auto [e] : edges_topological_sort(g)) { + ++count; + } + + REQUIRE(count == 0); +} + +TEST_CASE("edges_topological_sort - graph with no edges", "[topo][edges]") { + using Graph = std::vector>; + // Three isolated vertices + Graph g = {{}, {}, {}}; + + int count = 0; + for (auto [e] : edges_topological_sort(g)) { + ++count; + } + + REQUIRE(count == 0); +} + +//=========================================================================== +// Cycle detection tests +//=========================================================================== +// NOTE: Current implementation does not explicitly detect or reject cycles. +// On graphs with cycles, topological_sort produces an ordering, but it is +// NOT a valid topological ordering (some edges will point backward). +// These tests document the current behavior. +//=========================================================================== + +TEST_CASE("vertices_topological_sort - self-loop", "[topo][vertices][cycles]") { + using Graph = std::vector>; + // Vertex 0 has self-loop: 0 -> 0 + Graph g = {{0}}; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(vertex_id(g, v)); + } + + // Current behavior: produces ordering containing the vertex + REQUIRE(order.size() == 1); + REQUIRE(order[0] == 0); + + // However, this is NOT a valid topological sort because edge 0->0 exists + // and vertex 0 cannot come before itself +} + +TEST_CASE("vertices_topological_sort - simple cycle", "[topo][vertices][cycles]") { + using Graph = std::vector>; + // Simple cycle: 0 -> 1 -> 2 -> 0 + Graph g = {{1}, {2}, {0}}; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(vertex_id(g, v)); + } + + // Current behavior: produces an ordering with all vertices + REQUIRE(order.size() == 3); + + // Verify all vertices present + std::set vertices_seen(order.begin(), order.end()); + REQUIRE(vertices_seen.size() == 3); + REQUIRE(vertices_seen.count(0) == 1); + REQUIRE(vertices_seen.count(1) == 1); + REQUIRE(vertices_seen.count(2) == 1); + + // Check if any edge violates topological ordering + std::map positions; + for (std::size_t i = 0; i < order.size(); ++i) { + positions[order[i]] = static_cast(i); + } + + // At least one edge must point backward due to cycle + bool has_backward_edge = false; + for (std::size_t src = 0; src < g.size(); ++src) { + for (int tgt : g[src]) { + if (positions[static_cast(src)] >= positions[tgt]) { + has_backward_edge = true; + break; + } + } + if (has_backward_edge) break; + } + + // Document that cycle results in invalid topological ordering + REQUIRE(has_backward_edge); +} + +TEST_CASE("vertices_topological_sort - cycle with tail", "[topo][vertices][cycles]") { + using Graph = std::vector>; + // DAG leading into cycle: 0 -> 1 -> 2 -> 3 -> 1 (cycle: 1->2->3->1) + Graph g = {{1}, {2}, {3}, {1}}; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 4); + + // Vertex 0 should come before vertex 1 (acyclic part) + auto pos_0 = std::find(order.begin(), order.end(), 0) - order.begin(); + auto pos_1 = std::find(order.begin(), order.end(), 1) - order.begin(); + REQUIRE(pos_0 < pos_1); + + // But the cycle (1->2->3->1) means at least one edge must be backward + std::map positions; + for (std::size_t i = 0; i < order.size(); ++i) { + positions[order[i]] = static_cast(i); + } + + bool has_backward_edge = false; + if (positions[1] >= positions[2] || + positions[2] >= positions[3] || + positions[3] >= positions[1]) { + has_backward_edge = true; + } + + REQUIRE(has_backward_edge); +} + +TEST_CASE("vertices_topological_sort - multiple cycles", "[topo][vertices][cycles]") { + using Graph = std::vector>; + // Two separate cycles: (0->1->0) and (2->3->2) + Graph g = {{1}, {0}, {3}, {2}}; + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(vertex_id(g, v)); + } + + // All vertices should be present + REQUIRE(order.size() == 4); + + std::set vertices_seen(order.begin(), order.end()); + REQUIRE(vertices_seen.size() == 4); +} + +TEST_CASE("edges_topological_sort - simple cycle", "[topo][edges][cycles]") { + using Graph = std::vector>; + // Cycle: 0 -> 1 -> 2 -> 0 + Graph g = {{1}, {2}, {0}}; + + std::vector> edges; + for (auto [e] : edges_topological_sort(g)) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + // All 3 edges should be present + REQUIRE(edges.size() == 3); + + // Verify all edges present + std::set> edge_set(edges.begin(), edges.end()); + REQUIRE(edge_set.count({0, 1}) == 1); + REQUIRE(edge_set.count({1, 2}) == 1); + REQUIRE(edge_set.count({2, 0}) == 1); +} + +TEST_CASE("edges_topological_sort - self-loop", "[topo][edges][cycles]") { + using Graph = std::vector>; + // Self-loop: 0 -> 0 + Graph g = {{0}}; + + std::vector> edges; + for (auto [e] : edges_topological_sort(g)) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + // Self-loop edge should be present + REQUIRE(edges.size() == 1); + REQUIRE(edges[0].first == 0); + REQUIRE(edges[0].second == 0); +} + +TEST_CASE("topological_sort - cycle detection documentation", "[topo][cycles][.][documentation]") { + // This test documents the current behavior regarding cycles. + // + // CURRENT BEHAVIOR: + // - topological_sort does NOT detect or reject cycles + // - On cyclic graphs, it produces an ordering that includes all vertices + // - The ordering is NOT a valid topological sort + // - Some edges will point "backward" (from later to earlier positions) + // + // RATIONALE: + // - DFS-based implementation visits all reachable vertices + // - Cycle detection would require additional tracking (e.g., "on stack" marks) + // - For performance, the current implementation prioritizes speed over validation + // + // USER RESPONSIBILITY: + // - Users should ensure input graph is a DAG if they need valid topological ordering + // - For cycle detection, users should use a dedicated cycle detection algorithm + // - Behavior on cyclic graphs is well-defined but produces invalid orderings + // + // FUTURE CONSIDERATION: + // - Could add optional cycle detection with a flag or separate function + // - Could throw exception or return std::optional/expected + // - Would add overhead to the common (acyclic) case + + SUCCEED("Documentation test - no actual test needed"); +} + +//=========================================================================== +// Safe topological sort tests (with cycle detection via tl::expected) +//=========================================================================== + +TEST_CASE("vertices_topological_sort_safe - valid DAG", "[topo][vertices][safe]") { + using Graph = std::vector>; + // Simple DAG: 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + auto result = vertices_topological_sort_safe(g); + + REQUIRE(result.has_value()); + + std::vector order; + for (auto [v] : result.value()) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 3); + REQUIRE(order[0] == 0); + REQUIRE(order[1] == 1); + REQUIRE(order[2] == 2); +} + +TEST_CASE("vertices_topological_sort_safe - detects simple cycle", "[topo][vertices][safe]") { + using Graph = std::vector>; + // Cycle: 0 -> 1 -> 2 -> 0 + Graph g = {{1}, {2}, {0}}; + + auto result = vertices_topological_sort_safe(g); + + REQUIRE(!result.has_value()); + + // Should return the vertex that closes the cycle + auto cycle_vertex = result.error(); + auto cycle_id = vertex_id(g, cycle_vertex); + + // The cycle is detected at vertex 0 (where back edge points) + REQUIRE(cycle_id == 0); +} + +TEST_CASE("vertices_topological_sort_safe - detects self-loop", "[topo][vertices][safe]") { + using Graph = std::vector>; + // Self-loop: 0 -> 0 + Graph g = {{0}}; + + auto result = vertices_topological_sort_safe(g); + + REQUIRE(!result.has_value()); + REQUIRE(vertex_id(g, result.error()) == 0); +} + +TEST_CASE("vertices_topological_sort_safe - with value function on DAG", "[topo][vertices][safe]") { + using Graph = std::vector>; + // DAG: 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + auto result = vertices_topological_sort_safe(g, [](auto v) { return 42; }); + + REQUIRE(result.has_value()); + + std::vector> results; + for (auto [v, val] : result.value()) { + results.emplace_back(vertex_id(g, v), val); + } + + REQUIRE(results.size() == 3); + for (const auto& [vid, val] : results) { + REQUIRE(val == 42); + } +} + +TEST_CASE("vertices_topological_sort_safe - with value function on cycle", "[topo][vertices][safe]") { + using Graph = std::vector>; + // Cycle: 0 -> 1 -> 2 -> 0 + Graph g = {{1}, {2}, {0}}; + + auto result = vertices_topological_sort_safe(g, [](auto v) { return 99; }); + + REQUIRE(!result.has_value()); +} + +TEST_CASE("vertices_topological_sort_safe - detects cycle with tail", "[topo][vertices][safe]") { + using Graph = std::vector>; + // DAG leading into cycle: 0 -> 1 -> 2 -> 3 -> 1 + Graph g = {{1}, {2}, {3}, {1}}; + + auto result = vertices_topological_sort_safe(g); + + REQUIRE(!result.has_value()); + + // Cycle detected at vertex 1 + REQUIRE(vertex_id(g, result.error()) == 1); +} + +TEST_CASE("vertices_topological_sort_safe - diamond DAG", "[topo][vertices][safe]") { + using Graph = std::vector>; + // 0 + // / \ + // 1 2 + // \ / + // 3 + Graph g = {{1, 2}, {3}, {3}, {}}; + + auto result = vertices_topological_sort_safe(g); + + REQUIRE(result.has_value()); + + std::vector order; + for (auto [v] : *result) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 4); + REQUIRE(order[0] == 0); + REQUIRE(order[3] == 3); +} + +TEST_CASE("edges_topological_sort_safe - valid DAG", "[topo][edges][safe]") { + using Graph = std::vector>; + // Simple DAG: 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + auto result = edges_topological_sort_safe(g); + + REQUIRE(result.has_value()); + + std::vector> edges; + for (auto [e] : result.value()) { + edges.emplace_back(source_id(g, e), target_id(g, e)); + } + + REQUIRE(edges.size() == 2); + REQUIRE(edges[0] == std::make_pair(0, 1)); + REQUIRE(edges[1] == std::make_pair(1, 2)); +} + +TEST_CASE("edges_topological_sort_safe - detects cycle", "[topo][edges][safe]") { + using Graph = std::vector>; + // Cycle: 0 -> 1 -> 2 -> 0 + Graph g = {{1}, {2}, {0}}; + + auto result = edges_topological_sort_safe(g); + + REQUIRE(!result.has_value()); + REQUIRE(vertex_id(g, result.error()) == 0); +} + +TEST_CASE("edges_topological_sort_safe - with value function", "[topo][edges][safe]") { + using Graph = std::vector>; + // DAG: 0 -> 1 -> 2 + Graph g = {{1}, {2}, {}}; + + auto result = edges_topological_sort_safe(g, [](auto e) { return 7; }); + + REQUIRE(result.has_value()); + + int count = 0; + for (auto [e, val] : *result) { + REQUIRE(val == 7); + ++count; + } + + REQUIRE(count == 2); +} + +TEST_CASE("topological_sort_safe - usage patterns", "[topo][safe][.][documentation]") { + using Graph = std::vector>; + + // Example 1: Basic error checking + { + Graph g = {{1}, {2}, {0}}; // Cycle + auto result = vertices_topological_sort_safe(g); + + if (result) { + // Process successful result + for (auto [v] : *result) { + (void)v; // Process vertex + } + } else { + // Handle cycle + auto cycle_v = result.error(); + [[maybe_unused]] auto id = vertex_id(g, cycle_v); + // Log error, trace cycle, etc. + } + } + + // Example 2: Using operator bool + { + Graph g = {{1}, {2}, {}}; // DAG + auto result = vertices_topological_sort_safe(g); + + REQUIRE(result.has_value()); + REQUIRE(static_cast(result)); + } + + // Example 3: Monadic operations (if needed) + { + Graph g = {{1}, {2}, {}}; // DAG + auto result = vertices_topological_sort_safe(g); + + // Can use .value(), .error(), operator*, etc. + if (result) { + [[maybe_unused]] auto& view = result.value(); + // Or: auto& view = *result; + } + } + + SUCCEED("Documentation test demonstrating usage patterns"); +} diff --git a/tests/views/test_unified_header.cpp b/tests/views/test_unified_header.cpp new file mode 100644 index 0000000..ce4a5db --- /dev/null +++ b/tests/views/test_unified_header.cpp @@ -0,0 +1,153 @@ +/** + * @file test_unified_header.cpp + * @brief Tests for unified views.hpp header + */ + +#include + +// Test that single include works +#include + +#include + +using namespace graph; +using namespace graph::views::adaptors; + +// Simple test graph using vector-of-vectors +// Graph structure: 0 -> {1, 2}, 1 -> {2}, 2 -> {} +using test_graph = std::vector>; + +inline auto make_test_graph() { + test_graph g(3); // 3 vertices + g[0] = {1, 2}; // vertex 0 connects to vertices 1 and 2 + g[1] = {2}; // vertex 1 connects to vertex 2 + g[2] = {}; // vertex 2 has no outgoing edges + return g; +} + +TEST_CASE("unified header - all basic views accessible", "[unified][basic_views]") { + auto g = make_test_graph(); + + // Test that all basic views are accessible + int vertex_count = 0; + for (auto [v] : g | vertexlist()) { + ++vertex_count; + } + REQUIRE(vertex_count == 3); + + int edge_count = 0; + for (auto [e] : g | incidence(0)) { + ++edge_count; + } + REQUIRE(edge_count == 2); + + int neighbor_count = 0; + for (auto [n] : g | neighbors(0)) { + ++neighbor_count; + } + REQUIRE(neighbor_count == 2); + + int total_edges = 0; + for (auto [e] : g | edgelist()) { + ++total_edges; + } + REQUIRE(total_edges == 3); +} + +TEST_CASE("unified header - all search views accessible", "[unified][search_views]") { + auto g = make_test_graph(); + + // Test DFS views + int dfs_vertices = 0; + for (auto [v] : g | vertices_dfs(0)) { + ++dfs_vertices; + } + REQUIRE(dfs_vertices == 3); + + int dfs_edges = 0; + for (auto [e] : g | edges_dfs(0)) { + ++dfs_edges; + } + REQUIRE(dfs_edges == 2); // DFS tree has 2 edges (0->1, 1->2) + + // Test BFS views + int bfs_vertices = 0; + for (auto [v] : g | vertices_bfs(0)) { + ++bfs_vertices; + } + REQUIRE(bfs_vertices == 3); + + int bfs_edges = 0; + for (auto [e] : g | edges_bfs(0)) { + ++bfs_edges; + } + REQUIRE(bfs_edges == 2); // BFS tree also has 2 edges + + // Test topological sort views + int topo_vertices = 0; + for (auto [v] : g | vertices_topological_sort()) { + ++topo_vertices; + } + REQUIRE(topo_vertices == 3); + + int topo_edges = 0; + for (auto [e] : g | edges_topological_sort()) { + ++topo_edges; + } + REQUIRE(topo_edges == 3); +} + +TEST_CASE("unified header - value functions work", "[unified][value_functions]") { + auto g = make_test_graph(); + + auto vvf = [&g](auto v) { return vertex_id(g, v) * 10; }; + + std::vector values; + for (auto [v, val] : g | vertexlist(vvf)) { + values.push_back(val); + } + + REQUIRE(values.size() == 3); + REQUIRE(values[0] == 0); + REQUIRE(values[1] == 10); + REQUIRE(values[2] == 20); +} + +TEST_CASE("unified header - chaining with std::views works", "[unified][chaining]") { + auto g = make_test_graph(); + + // Test complex chaining + std::vector results; + for (auto id : g | vertexlist() + | std::views::transform([&g](auto info) { + auto [v] = info; + return vertex_id(g, v); + }) + | std::views::filter([](int id) { return id > 0; }) + | std::views::transform([](int id) { return id * 2; })) { + results.push_back(id); + } + + REQUIRE(results.size() == 2); + REQUIRE(results[0] == 2); // vertex 1 * 2 + REQUIRE(results[1] == 4); // vertex 2 * 2 +} + +TEST_CASE("unified header - direct calls work", "[unified][direct_calls]") { + auto g = make_test_graph(); + + // Test that direct calls (without pipes) also work + auto vertex_view = graph::views::vertexlist(g); + int count = 0; + for (auto [v] : vertex_view) { + ++count; + } + REQUIRE(count == 3); + + auto dfs_view = graph::views::vertices_dfs(g, 0); + count = 0; + for (auto [v] : dfs_view) { + ++count; + } + REQUIRE(count == 3); +} diff --git a/tests/views/test_vertex_info.cpp b/tests/views/test_vertex_info.cpp new file mode 100644 index 0000000..4dfe29f --- /dev/null +++ b/tests/views/test_vertex_info.cpp @@ -0,0 +1,250 @@ +#include +#include +#include + +using namespace graph; + +// Mock types for testing +struct mock_vertex_descriptor { + int id; +}; + +struct mock_value { + double data; +}; + +TEST_CASE("vertex_info: all 8 specializations compile", "[vertex_info]") { + SECTION("VId, V, VV all present") { + vertex_info vi{1, mock_vertex_descriptor{1}, mock_value{42.0}}; + REQUIRE(vi.id == 1); + REQUIRE(vi.vertex.id == 1); + REQUIRE(vi.value.data == 42.0); + } + + SECTION("VId, V present; VV=void") { + vertex_info vi{2, mock_vertex_descriptor{2}}; + REQUIRE(vi.id == 2); + REQUIRE(vi.vertex.id == 2); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId, VV present; V=void") { + vertex_info vi{3, mock_value{99.9}}; + REQUIRE(vi.id == 3); + REQUIRE(vi.value.data == 99.9); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId present; V=void, VV=void") { + vertex_info vi{4}; + REQUIRE(vi.id == 4); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void; V, VV present (descriptor-based pattern)") { + vertex_info vi{mock_vertex_descriptor{5}, mock_value{123.4}}; + REQUIRE(vi.vertex.id == 5); + REQUIRE(vi.value.data == 123.4); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, VV=void; V present") { + vertex_info vi{mock_vertex_descriptor{6}}; + REQUIRE(vi.vertex.id == 6); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, V=void; VV present") { + vertex_info vi{mock_value{77.7}}; + REQUIRE(vi.value.data == 77.7); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("VId=void, V=void, VV=void (empty)") { + vertex_info vi{}; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("vertex_info: structured bindings work correctly", "[vertex_info]") { + SECTION("All three members") { + vertex_info vi{1, mock_vertex_descriptor{1}, mock_value{42.0}}; + auto [id, v, val] = vi; + REQUIRE(id == 1); + REQUIRE(v.id == 1); + REQUIRE(val.data == 42.0); + } + + SECTION("Two members: id and vertex") { + vertex_info vi{2, mock_vertex_descriptor{2}}; + auto [id, v] = vi; + REQUIRE(id == 2); + REQUIRE(v.id == 2); + } + + SECTION("Two members: id and value") { + vertex_info vi{3, mock_value{99.9}}; + auto [id, val] = vi; + REQUIRE(id == 3); + REQUIRE(val.data == 99.9); + } + + SECTION("One member: id only") { + vertex_info vi{4}; + auto [id] = vi; + REQUIRE(id == 4); + } + + SECTION("Two members: vertex and value (descriptor-based)") { + vertex_info vi{mock_vertex_descriptor{5}, mock_value{123.4}}; + auto [v, val] = vi; + REQUIRE(v.id == 5); + REQUIRE(val.data == 123.4); + } + + SECTION("One member: vertex only") { + vertex_info vi{mock_vertex_descriptor{6}}; + auto [v] = vi; + REQUIRE(v.id == 6); + } + + SECTION("One member: value only") { + vertex_info vi{mock_value{77.7}}; + auto [val] = vi; + REQUIRE(val.data == 77.7); + } +} + +TEST_CASE("vertex_info: sizeof verifies physical absence of void members", "[vertex_info]") { + SECTION("Full struct vs VId=void reduces size") { + using full_t = vertex_info; + using no_id_t = vertex_info; + + // No id member should be smaller or equal (padding may prevent strict reduction) + REQUIRE(sizeof(no_id_t) <= sizeof(full_t)); + // Should be at most the size of the two members plus padding + REQUIRE(sizeof(no_id_t) <= sizeof(mock_vertex_descriptor) + sizeof(mock_value) + sizeof(int)); + } + + SECTION("VId only struct") { + using id_only_t = vertex_info; + REQUIRE(sizeof(id_only_t) == sizeof(int)); + } + + SECTION("Empty struct") { + using empty_t = vertex_info; + // Empty struct has size 1 in C++ (must be distinct) + REQUIRE(sizeof(empty_t) >= 1); + } +} + +TEST_CASE("vertex_info: copyable and movable", "[vertex_info]") { + SECTION("Copy construction") { + vertex_info vi1{1, mock_vertex_descriptor{1}, mock_value{42.0}}; + vertex_info vi2 = vi1; + REQUIRE(vi2.id == vi1.id); + REQUIRE(vi2.vertex.id == vi1.vertex.id); + REQUIRE(vi2.value.data == vi1.value.data); + } + + SECTION("Move construction") { + vertex_info vi1{2, mock_vertex_descriptor{2}, mock_value{99.9}}; + vertex_info vi2 = std::move(vi1); + REQUIRE(vi2.id == 2); + REQUIRE(vi2.vertex.id == 2); + REQUIRE(vi2.value.data == 99.9); + } +} + +TEST_CASE("vertex_info: descriptor-based pattern primary use case", "[vertex_info]") { + // This is the primary pattern for views: vertex_info + SECTION("Descriptor with value function") { + vertex_info vi{mock_vertex_descriptor{10}, 42}; + + auto [v, val] = vi; + REQUIRE(v.id == 10); + REQUIRE(val == 42); + + // Verify no id member present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Descriptor without value function") { + vertex_info vi{mock_vertex_descriptor{20}}; + + auto [v] = vi; + REQUIRE(v.id == 20); + + // Verify only vertex member present + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("vertex_info: external data pattern use case", "[vertex_info]") { + // This pattern is for external data: vertex_info + SECTION("ID and value for graph construction") { + vertex_info vi{42, std::string("vertex_data")}; + + auto [id, val] = vi; + REQUIRE(id == 42); + REQUIRE(val == "vertex_data"); + + // Verify no vertex member present + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("ID only for lightweight iteration") { + vertex_info vi{123}; + + auto [id] = vi; + REQUIRE(id == 123); + + // Verify only id member present + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + } +} + +TEST_CASE("vertex_info: type traits are correct", "[vertex_info]") { + SECTION("All type aliases match template parameters") { + using vi_t = vertex_info; + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Void type aliases when void") { + using vi_t = vertex_info; + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEST_CASE("vertex_info: copyable_vertex_t alias works", "[vertex_info]") { + SECTION("Alias matches specialized form") { + using alias_t = copyable_vertex_t; + using explicit_t = vertex_info; + + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("Alias used for external data") { + copyable_vertex_t cv{99, std::string("data")}; + auto [id, val] = cv; + REQUIRE(id == 99); + REQUIRE(val == "data"); + } +} diff --git a/tests/views/test_vertexlist.cpp b/tests/views/test_vertexlist.cpp new file mode 100644 index 0000000..37f1cc6 --- /dev/null +++ b/tests/views/test_vertexlist.cpp @@ -0,0 +1,621 @@ +/** + * @file test_vertexlist.cpp + * @brief Comprehensive tests for vertexlist view + */ + +#include +#include + +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +// ============================================================================= +// Test 1: Empty Graph +// ============================================================================= + +TEST_CASE("vertexlist - empty graph", "[vertexlist][empty]") { + using Graph = std::vector>; + Graph g; + + SECTION("no value function") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 0); + REQUIRE(vlist.begin() == vlist.end()); + + std::size_t count = 0; + for ([[maybe_unused]] auto vi : vlist) { + ++count; + } + REQUIRE(count == 0); + } + + SECTION("with value function") { + auto vlist = vertexlist(g, [](auto v) { return v.vertex_id(); }); + + REQUIRE(vlist.size() == 0); + REQUIRE(vlist.begin() == vlist.end()); + } +} + +// ============================================================================= +// Test 2: Single Vertex +// ============================================================================= + +TEST_CASE("vertexlist - single vertex", "[vertexlist][single]") { + using Graph = std::vector>; + Graph g = {{}}; // One vertex with no edges + + SECTION("no value function") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 1); + + auto it = vlist.begin(); + REQUIRE(it != vlist.end()); + + auto vi = *it; + REQUIRE(vi.vertex.vertex_id() == 0); + + ++it; + REQUIRE(it == vlist.end()); + } + + SECTION("with value function returning vertex_id") { + auto vlist = vertexlist(g, [](auto v) { return v.vertex_id() * 2; }); + + REQUIRE(vlist.size() == 1); + + auto vi = *vlist.begin(); + REQUIRE(vi.vertex.vertex_id() == 0); + REQUIRE(vi.value == 0); // 0 * 2 = 0 + } +} + +// ============================================================================= +// Test 3: Multiple Vertices +// ============================================================================= + +TEST_CASE("vertexlist - multiple vertices", "[vertexlist][multiple]") { + using Graph = std::vector>; + Graph g = { + {1, 2}, // vertex 0 -> edges to 1, 2 + {2, 3}, // vertex 1 -> edges to 2, 3 + {3}, // vertex 2 -> edge to 3 + {} // vertex 3 -> no edges + }; + + SECTION("no value function - iteration") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 4); + + std::vector ids; + for (auto vi : vlist) { + ids.push_back(vi.vertex.vertex_id()); + } + + REQUIRE(ids == std::vector{0, 1, 2, 3}); + } + + SECTION("with value function") { + auto vlist = vertexlist(g, [](auto v) { + return static_cast(v.vertex_id() * 10); + }); + + std::vector values; + for (auto vi : vlist) { + values.push_back(vi.value); + } + + REQUIRE(values == std::vector{0, 10, 20, 30}); + } + + SECTION("structured binding - no value function") { + auto vlist = vertexlist(g); + + std::size_t idx = 0; + for (auto [v] : vlist) { + REQUIRE(v.vertex_id() == idx); + ++idx; + } + REQUIRE(idx == 4); + } + + SECTION("structured binding - with value function") { + auto vlist = vertexlist(g, [&g](auto v) { + return g[v.vertex_id()].size(); // number of edges + }); + + std::vector edge_counts; + for (auto [v, count] : vlist) { + edge_counts.push_back(count); + } + + REQUIRE(edge_counts == std::vector{2, 2, 1, 0}); + } +} + +// ============================================================================= +// Test 4: Value Function Types +// ============================================================================= + +TEST_CASE("vertexlist - value function types", "[vertexlist][vvf]") { + using Graph = std::vector>; + Graph g = {{1}, {2}, {}}; + + SECTION("returning string") { + auto vlist = vertexlist(g, [](auto v) { + return "vertex_" + std::to_string(v.vertex_id()); + }); + + std::vector names; + for (auto [v, name] : vlist) { + names.push_back(name); + } + + REQUIRE(names == std::vector{"vertex_0", "vertex_1", "vertex_2"}); + } + + SECTION("returning double") { + auto vlist = vertexlist(g, [](auto v) { + return static_cast(v.vertex_id()) * 1.5; + }); + + std::vector values; + for (auto [v, val] : vlist) { + values.push_back(val); + } + + REQUIRE(values[0] == 0.0); + REQUIRE(values[1] == 1.5); + REQUIRE(values[2] == 3.0); + } + + SECTION("capturing lambda") { + std::vector labels = {"A", "B", "C"}; + + auto vlist = vertexlist(g, [&labels](auto v) { + return labels[v.vertex_id()]; + }); + + std::vector result; + for (auto [v, label] : vlist) { + result.push_back(label); + } + + REQUIRE(result == std::vector{"A", "B", "C"}); + } + + SECTION("mutable lambda") { + int counter = 0; + auto vlist = vertexlist(g, [&counter](auto) mutable { + return counter++; + }); + + std::vector values; + for (auto [v, val] : vlist) { + values.push_back(val); + } + + REQUIRE(values == std::vector{0, 1, 2}); + } +} + +// ============================================================================= +// Test 5: Deque-based Graph +// ============================================================================= + +TEST_CASE("vertexlist - deque-based graph", "[vertexlist][deque]") { + using Graph = std::deque>; + Graph g = { + {1}, + {2}, + {0} + }; + + SECTION("no value function") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 3); + + std::vector ids; + for (auto [v] : vlist) { + ids.push_back(v.vertex_id()); + } + + REQUIRE(ids == std::vector{0, 1, 2}); + } + + SECTION("with value function") { + auto vlist = vertexlist(g, [&g](auto v) { + return g[v.vertex_id()].front(); // first edge target + }); + + std::vector targets; + for (auto [v, target] : vlist) { + targets.push_back(target); + } + + REQUIRE(targets == std::vector{1, 2, 0}); + } +} + +// ============================================================================= +// Test 6: Range Concepts +// ============================================================================= + +TEST_CASE("vertexlist - range concepts", "[vertexlist][concepts]") { + using Graph = std::vector>; + using ViewNoVVF = vertexlist_view; + using ViewWithVVF = vertexlist_view; + + SECTION("input_range satisfied") { + STATIC_REQUIRE(std::ranges::input_range); + STATIC_REQUIRE(std::ranges::input_range); + } + + SECTION("forward_range satisfied") { + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::ranges::forward_range); + } + + SECTION("sized_range satisfied") { + STATIC_REQUIRE(std::ranges::sized_range); + STATIC_REQUIRE(std::ranges::sized_range); + } + + SECTION("view satisfied") { + STATIC_REQUIRE(std::ranges::view); + STATIC_REQUIRE(std::ranges::view); + } +} + +// ============================================================================= +// Test 7: Iterator Properties +// ============================================================================= + +TEST_CASE("vertexlist - iterator properties", "[vertexlist][iterator]") { + using Graph = std::vector>; + Graph g = {{1, 2}, {2}, {}}; + + SECTION("pre-increment") { + auto vlist = vertexlist(g); + auto it = vlist.begin(); + + REQUIRE((*it).vertex.vertex_id() == 0); + ++it; + REQUIRE((*it).vertex.vertex_id() == 1); + ++it; + REQUIRE((*it).vertex.vertex_id() == 2); + ++it; + REQUIRE(it == vlist.end()); + } + + SECTION("post-increment") { + auto vlist = vertexlist(g); + auto it = vlist.begin(); + + auto old = it++; + REQUIRE((*old).vertex.vertex_id() == 0); + REQUIRE((*it).vertex.vertex_id() == 1); + } + + SECTION("equality comparison") { + auto vlist = vertexlist(g); + auto it1 = vlist.begin(); + auto it2 = vlist.begin(); + + REQUIRE(it1 == it2); + + ++it1; + REQUIRE(it1 != it2); + + ++it2; + REQUIRE(it1 == it2); + } + + SECTION("default constructed iterators are equal") { + using Iter = decltype(vertexlist(g).begin()); + Iter it1; + Iter it2; + REQUIRE(it1 == it2); + } +} + +// ============================================================================= +// Test 8: vertex_info Types +// ============================================================================= + +TEST_CASE("vertexlist - vertex_info types", "[vertexlist][info]") { + using Graph = std::vector>; + using VertexType = vertex_t; + + SECTION("no value function - info type") { + using ViewType = vertexlist_view; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_void_v); + } + + SECTION("with value function - info type") { + auto vvf = [](auto) { return 42; }; + using VVFType = decltype(vvf); + using ViewType = vertexlist_view; + using InfoType = typename ViewType::info_type; + + STATIC_REQUIRE(std::is_void_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +// ============================================================================= +// Test 9: Const Graph Access +// ============================================================================= + +TEST_CASE("vertexlist - const graph", "[vertexlist][const]") { + using Graph = std::vector>; + const Graph g = {{1}, {2}, {}}; + + SECTION("no value function") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 3); + + std::size_t count = 0; + for (auto [v] : vlist) { + REQUIRE(v.vertex_id() == count); + ++count; + } + REQUIRE(count == 3); + } + + SECTION("with value function") { + auto vlist = vertexlist(g, [](auto v) { return v.vertex_id(); }); + + std::vector ids; + for (auto [v, id] : vlist) { + ids.push_back(id); + } + + REQUIRE(ids == std::vector{0, 1, 2}); + } +} + +// ============================================================================= +// Test 10: Weighted Graph (pair edges) +// ============================================================================= + +TEST_CASE("vertexlist - weighted graph", "[vertexlist][weighted]") { + using Graph = std::vector>>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 + {{2, 3.5}}, // vertex 1 + {} // vertex 2 + }; + + SECTION("iteration works with pair edges") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 3); + + std::vector ids; + for (auto [v] : vlist) { + ids.push_back(v.vertex_id()); + } + + REQUIRE(ids == std::vector{0, 1, 2}); + } + + SECTION("value function can access edge data") { + auto vlist = vertexlist(g, [&g](auto v) { + // Sum of edge weights for this vertex + double sum = 0.0; + for (auto [target, weight] : g[v.vertex_id()]) { + sum += weight; + } + return sum; + }); + + std::vector sums; + for (auto [v, sum] : vlist) { + sums.push_back(sum); + } + + REQUIRE(sums[0] == 4.0); // 1.5 + 2.5 + REQUIRE(sums[1] == 3.5); + REQUIRE(sums[2] == 0.0); + } +} + +// ============================================================================= +// Test 11: ranges::distance +// ============================================================================= + +TEST_CASE("vertexlist - std::ranges algorithms", "[vertexlist][algorithms]") { + using Graph = std::vector>; + Graph g = {{1, 2}, {2}, {}, {0}}; + + SECTION("distance") { + auto vlist = vertexlist(g); + auto dist = std::ranges::distance(vlist); + REQUIRE(dist == 4); + } + + SECTION("count_if") { + auto vlist = vertexlist(g, [&g](auto v) { return g[v.vertex_id()].size(); }); + + auto count = std::ranges::count_if(vlist, [](auto vi) { + return vi.value > 0; + }); + + REQUIRE(count == 3); // vertices 0, 1, and 3 have edges + } +} + +// ============================================================================= +// Test 12: Map-Based Vertex Container (Sparse Vertex IDs) +// ============================================================================= + +TEST_CASE("vertexlist - map vertices vector edges", "[vertexlist][map]") { + // Map-based graphs have sparse, non-contiguous vertex IDs + using Graph = std::map>; + Graph g = { + {100, {200, 300}}, // vertex 100 -> edges to 200, 300 + {200, {300}}, // vertex 200 -> edge to 300 + {300, {}} // vertex 300 -> no edges + }; + + SECTION("iteration over sparse vertex IDs") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 3); + + std::vector ids; + for (auto [v] : vlist) { + ids.push_back(v.vertex_id()); + } + + // Map maintains sorted order + REQUIRE(ids == std::vector{100, 200, 300}); + } + + SECTION("with value function") { + auto vlist = vertexlist(g, [&g](auto v) { + // Return edge count for each vertex + return g.at(v.vertex_id()).size(); + }); + + std::vector edge_counts; + for (auto [v, count] : vlist) { + edge_counts.push_back(count); + } + + REQUIRE(edge_counts == std::vector{2, 1, 0}); + } + + SECTION("empty map graph") { + Graph empty_g; + auto vlist = vertexlist(empty_g); + + REQUIRE(vlist.size() == 0); + REQUIRE(vlist.begin() == vlist.end()); + } + + SECTION("single vertex map") { + Graph single_g = {{42, {}}}; + auto vlist = vertexlist(single_g); + + REQUIRE(vlist.size() == 1); + + auto [v] = *vlist.begin(); + REQUIRE(v.vertex_id() == 42); + } +} + +// ============================================================================= +// Test 13: Map-Based Edge Container (Sorted Edges) +// ============================================================================= + +TEST_CASE("vertexlist - vector vertices map edges", "[vertexlist][edge_map]") { + // Edges stored in map (sorted by target, deduplicated) + using Graph = std::vector>; + Graph g = { + {{1, 1.5}, {2, 2.5}}, // vertex 0 -> (1, 1.5), (2, 2.5) + {{2, 3.5}}, // vertex 1 -> (2, 3.5) + {} // vertex 2 -> no edges + }; + + SECTION("iteration") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 3); + + std::vector ids; + for (auto [v] : vlist) { + ids.push_back(v.vertex_id()); + } + + REQUIRE(ids == std::vector{0, 1, 2}); + } + + SECTION("with value function accessing edge weights") { + auto vlist = vertexlist(g, [&g](auto v) { + // Sum of edge weights for this vertex + double sum = 0.0; + for (auto& [target, weight] : g[v.vertex_id()]) { + sum += weight; + } + return sum; + }); + + std::vector sums; + for (auto [v, sum] : vlist) { + sums.push_back(sum); + } + + REQUIRE(sums[0] == 4.0); // 1.5 + 2.5 + REQUIRE(sums[1] == 3.5); + REQUIRE(sums[2] == 0.0); + } +} + +// ============================================================================= +// Test 14: Map Vertices + Map Edges (Fully Sparse Graph) +// ============================================================================= + +TEST_CASE("vertexlist - map vertices map edges", "[vertexlist][map][edge_map]") { + // Both vertices and edges in maps - fully sparse graph + using Graph = std::map>; + Graph g = { + {10, {{20, 1.0}, {30, 2.0}}}, // vertex 10 -> (20, 1.0), (30, 2.0) + {20, {{30, 3.0}}}, // vertex 20 -> (30, 3.0) + {30, {}} // vertex 30 -> no edges + }; + + SECTION("iteration") { + auto vlist = vertexlist(g); + + REQUIRE(vlist.size() == 3); + + std::vector ids; + for (auto [v] : vlist) { + ids.push_back(v.vertex_id()); + } + + REQUIRE(ids == std::vector{10, 20, 30}); + } + + SECTION("with value function") { + auto vlist = vertexlist(g, [&g](auto v) { + return g.at(v.vertex_id()).size(); + }); + + std::vector counts; + for (auto [v, count] : vlist) { + counts.push_back(count); + } + + REQUIRE(counts == std::vector{2, 1, 0}); + } + + SECTION("structured binding access") { + auto vlist = vertexlist(g, [](auto v) { return v.vertex_id() * 10; }); + + std::vector scaled_ids; + for (auto [v, scaled] : vlist) { + scaled_ids.push_back(scaled); + } + + REQUIRE(scaled_ids == std::vector{100, 200, 300}); + } +} diff --git a/tests/views/test_view_concepts.cpp b/tests/views/test_view_concepts.cpp new file mode 100644 index 0000000..4ddd8f6 --- /dev/null +++ b/tests/views/test_view_concepts.cpp @@ -0,0 +1,180 @@ +#include +#include +#include + +using namespace graph::views; + +// Mock descriptor types for testing +struct mock_vertex_descriptor { + std::size_t id; +}; + +struct mock_edge_descriptor { + std::size_t source; + std::size_t target; +}; + +// Mock value functions +auto valid_vertex_value_fn = [](mock_vertex_descriptor) { return 42; }; +auto valid_edge_value_fn = [](mock_edge_descriptor) { return 3.14; }; + +auto void_vertex_fn = [](mock_vertex_descriptor) {}; +auto void_edge_fn = [](mock_edge_descriptor) {}; + +struct not_invocable { + int x; +}; + +// Mock search view for testing +struct mock_search_view { + cancel_search cancel() { return cancel_search::continue_search; } + std::size_t depth() const { return 5; } + std::size_t size() const { return 10; } +}; + +struct incomplete_search_view { + std::size_t depth() const { return 5; } + std::size_t size() const { return 10; } + // Missing cancel() +}; + +TEST_CASE("vertex_value_function concept", "[views][concepts]") { + SECTION("valid value functions") { + // Lambda returning int + STATIC_REQUIRE(vertex_value_function); + + // Lambda returning string + auto string_fn = [](mock_vertex_descriptor) { return std::string("test"); }; + STATIC_REQUIRE( + vertex_value_function); + + // Function pointer + using fn_ptr = int (*)(mock_vertex_descriptor); + STATIC_REQUIRE(vertex_value_function); + + // Generic lambda + auto generic_fn = [](auto vdesc) { return vdesc.id; }; + STATIC_REQUIRE( + vertex_value_function); + } + + SECTION("invalid value functions") { + // Returns void + STATIC_REQUIRE_FALSE(vertex_value_function); + + // Not invocable + STATIC_REQUIRE_FALSE( + vertex_value_function); + + // Wrong parameter type + STATIC_REQUIRE_FALSE( + vertex_value_function); + } +} + +TEST_CASE("edge_value_function concept", "[views][concepts]") { + SECTION("valid value functions") { + // Lambda returning double + STATIC_REQUIRE( + edge_value_function); + + // Lambda returning string + auto string_fn = [](mock_edge_descriptor) { return std::string("edge"); }; + STATIC_REQUIRE( + edge_value_function); + + // Function pointer + using fn_ptr = double (*)(mock_edge_descriptor); + STATIC_REQUIRE(edge_value_function); + + // Generic lambda + auto generic_fn = [](auto edesc) { return edesc.source + edesc.target; }; + STATIC_REQUIRE( + edge_value_function); + } + + SECTION("invalid value functions") { + // Returns void + STATIC_REQUIRE_FALSE( + edge_value_function); + + // Not invocable + STATIC_REQUIRE_FALSE(edge_value_function); + + // Wrong parameter type + STATIC_REQUIRE_FALSE( + edge_value_function); + } +} + +TEST_CASE("search_view concept", "[views][concepts]") { + SECTION("valid search view") { + STATIC_REQUIRE(search_view); + + // Test at runtime that the mock actually works + mock_search_view view; + REQUIRE(view.cancel() == cancel_search::continue_search); + REQUIRE(view.depth() == 5); + REQUIRE(view.size() == 10); + } + + SECTION("invalid search view - missing cancel()") { + STATIC_REQUIRE_FALSE(search_view); + } + + SECTION("invalid search view - not a type with required methods") { + STATIC_REQUIRE_FALSE(search_view); + STATIC_REQUIRE_FALSE(search_view); + } +} + +TEST_CASE("concept interaction with actual types", "[views][concepts]") { + SECTION("value functions with different return types") { + // int return + auto int_fn = [](mock_vertex_descriptor) { return 42; }; + STATIC_REQUIRE( + vertex_value_function); + + // double return + auto double_fn = [](mock_vertex_descriptor) { return 3.14; }; + STATIC_REQUIRE( + vertex_value_function); + + // string return + auto string_fn = [](mock_vertex_descriptor) { return std::string("test"); }; + STATIC_REQUIRE( + vertex_value_function); + + // struct return + struct custom_value { + int x; + int y; + }; + auto struct_fn = [](mock_vertex_descriptor) { return custom_value{1, 2}; }; + STATIC_REQUIRE( + vertex_value_function); + } + + SECTION("mutable lambdas") { + // Mutable lambda (captures by value and modifies) + int counter = 0; + auto mutable_fn = [counter](mock_vertex_descriptor) mutable { + ++counter; + return counter; + }; + STATIC_REQUIRE( + vertex_value_function); + } + + SECTION("capturing lambdas") { + int multiplier = 10; + auto capturing_fn = [&multiplier](mock_vertex_descriptor vdesc) { + return static_cast(vdesc.id) * multiplier; + }; + STATIC_REQUIRE( + vertex_value_function); + } +}