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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
## [Unreleased]

### Added
- **Composable visitor toolkit** (`algorithm/visitor_factory.hpp`, included via `algorithms.hpp` umbrella) — BGL-style event-visitor combinators built on the existing duck-typed `on_*` callback model, without changing any traversal algorithm:
- **Layer 1 — single-event adaptors** (`on_discover_vertex(f)`, `on_tree_edge(f)`, …): wrap a callable so it fires for exactly one traversal event. Generated for all 14 event names (5 vertex + 9 edge) via a macro. Analogous to BGL event tags.
- **Layer 2 — `composite_visitor` / `make_visitor(...)`**: fan a single traversal out to multiple sub-visitors. Each event method is constrained by a fold over the child pack, so a composite exposes `on_X` only when at least one child handles it — keeping `has_on_*` / `valid_visitor` detection accurate and preserving zero-overhead event skipping. Bridges descriptor- and id-form children automatically via `vertex_id()`.
- **Layer 3 — prebuilt recorders**: `predecessor_recorder(pred)`, `distance_recorder(dist, weight_fn)` (weighted), `distance_recorder(dist)` (hop-count), and `time_stamper(time_map, clock)`. Each returns a `(g, x)->void` callable; the caller binds it to the desired event (e.g. `on_tree_edge(predecessor_recorder(pred))` for BFS/DFS or `on_edge_relaxed(predecessor_recorder(pred))` for Dijkstra/Bellman-Ford).
- 9 test cases in `tests/algorithms/test_visitor_factory.cpp` covering compile-time concept checks and runtime BFS/DFS/Dijkstra coverage.
- **`valid_visitor` strict concept** (`algorithm/traversal_common.hpp`) — `has_any_visitor_event<G, Visitor>` ORs all 19 `has_on_*` concepts; `valid_visitor<G, Visitor>` is satisfied either by `empty_visitor` or by any type with at least one recognized `on_*` method. A `static_assert(valid_visitor<...>)` is placed at the top of BFS, DFS, Dijkstra, and Bellman-Ford so misspelled event names produce a clear diagnostic instead of a silent no-op.
- **`filtered_graph` adaptor** (`adaptors/filtered_graph.hpp`) — non-owning wrapper that filters vertices and edges by predicate during traversal. Satisfies `adjacency_list` so all views and algorithms work transparently on the filtered subgraph. Uses self-contained `filtering_iterator` to avoid `std::views::filter` iterator dangling issues.
- **BGL graph adaptor** (`adaptors/bgl/graph_adaptor.hpp`) — adapts Boost.Graph types for use with graph-v3 CPOs, views, and algorithms. Includes `bgl_edge_iterator.hpp` (C++20 iterator wrapper) and `property_bridge.hpp` (BGL property maps → graph-v3 value functions). Supports adjacency_list, CSR, and bidirectional BGL graphs.
- `graph::adaptors::keep_all` — sentinel predicate (accepts everything, zero-overhead)
Expand Down
59 changes: 49 additions & 10 deletions agents/bgl_migration_strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A comprehensive analysis of the Boost Graph Library (BGL) and graph-v3, identifying gaps, migration paths, and recommended extensions to enable a smooth upgrade transition.

> **Last reviewed:** 2026-05-03 against `include/graph/` source tree.
> **Last reviewed:** 2026-05-30 against `include/graph/` source tree.

---

Expand Down Expand Up @@ -51,7 +51,6 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3
- No `copy_graph` utility with cross-type and property mapping support
- No `labeled_graph` adaptor (string labels → vertex mapping)
- No named parameter interface (BGL users must learn new positional API)
- No composable visitor adaptors (`make_visitor(...)` factory)

---

Expand Down Expand Up @@ -395,7 +394,7 @@ breadth_first_search(g, {s}, my_visitor{});
| **Missing events** | Base class provides no-ops | Simply omit the method — zero overhead |
| **Parameter order** | `(vertex, graph)` or `(edge, graph)` | `(graph, vertex_id)` or `(graph, edge)` — graph first |
| **Vertex parameter** | `vertex_descriptor` | Both `vertex_id_t<G>` and `vertex_t<G>` overloads supported |
| **Event composition** | `make_bfs_visitor(pair<recorder1, pair<recorder2, ...>>)` | Not availablewrite a combined visitor struct |
| **Event composition** | `make_bfs_visitor(pair<recorder1, pair<recorder2, ...>>)` | `make_visitor(on_tree_edge(predecessor_recorder(pred)), ...)`see `visitor_factory.hpp` |

### BGL Event → graph-v3 Event Mapping

Expand All @@ -420,18 +419,58 @@ breadth_first_search(g, {s}, my_visitor{});

**Missing events:** `non_tree_edge`, `gray_target`, `black_target` are BFS-specific events that distinguish edge targets by color state. These could be added to graph-v3's BFS visitor concept if needed for migration.

### Composable Visitor Adaptors — Gap (still open)
### Composable Visitor Adaptors — Implemented

BGL provides reusable event visitor adaptors (`predecessor_recorder`, `distance_recorder`, `time_stamper`, `property_writer`) that can be composed via `std::pair` chaining. graph-v3 has no equivalent — users write monolithic visitor structs. As of 2026-05, no `make_visitor(...)` factory or pre-built event adaptors are available.
BGL provides reusable event visitor adaptors (`predecessor_recorder`, `distance_recorder`, `time_stamper`, `property_writer`) that can be composed via `std::pair` chaining. graph-v3 now provides an equivalent toolkit in `include/graph/algorithm/visitor_factory.hpp` (included by the `graph/algorithms.hpp` umbrella).

**Recommendation:** Consider providing lambda-based visitor construction:
Three layers:

**Layer 1 — single-event adaptors.** Wrap a callable so it fires for exactly one traversal event, analogous to BGL event tags:
```cpp
on_discover_vertex(f) // vertex events
on_tree_edge(f) // edge events — on_tree_edge, on_back_edge,
// on_edge_relaxed, on_finish_edge, etc.
```

**Layer 2 — `make_visitor(...)`.** Fan one traversal out to any number of sub-visitors. Each child may be a single-event adaptor, a prebuilt recorder, or any struct with `on_*` methods. Supports mixing descriptor-form and id-form children in the same call:
```cpp
auto vis = make_visitor(
on_discover_vertex([&](auto& g, auto uid) { ... }),
on_examine_edge([&](auto& g, auto uv) { ... })
on_discover_vertex([&](auto& g, auto uid) { order.push_back(uid); }),
on_tree_edge(predecessor_recorder(pred))
);
breadth_first_search(g, {s}, vis);
```

**Layer 3 — prebuilt recorders.** Ready-made callables for common bookkeeping; the caller binds each to the desired event:

| Recorder | Description | Typical event binding |
|----------|-------------|----------------------|
| `predecessor_recorder(pred)` | `pred[target_id] = source_id` | `on_tree_edge` (BFS/DFS), `on_edge_relaxed` (Dijkstra) |
| `distance_recorder(dist, weight_fn)` | weighted distance accumulation | `on_edge_relaxed` |
| `distance_recorder(dist)` | hop-count distance | `on_tree_edge` |
| `time_stamper(time_map, clock)` | `time[vertex_id] = clock++` | `on_discover_vertex`, `on_finish_vertex` |

**BGL vs. graph-v3 comparison:**
```cpp
// BGL:
breadth_first_search(g, s, visitor(make_bfs_visitor(std::make_pair(
record_predecessors(pred.data(), on_tree_edge()),
record_distances(dist.data(), on_tree_edge())))));

// graph-v3:
breadth_first_search(g, {s},
make_visitor(
on_tree_edge(predecessor_recorder(pred)),
on_tree_edge(distance_recorder(dist)),
on_discover_vertex([&](auto&, auto uid){ order.push_back(uid); })));
```

Key differences from BGL:
- Recorders are plain `(g, x)->void` callables bound to events by the caller — no event-tag type system.
- Visitors are held by reference, not copied; no `boost::ref` wrapper needed for stateful visitors.
- `composite_visitor` exposes `on_X` only when at least one child handles event `X`, keeping `has_on_*` / `valid_visitor` detection accurate.
- `property_writer` (BGL) has no direct equivalent; use a lambda with `on_examine_vertex` / `on_examine_edge`.

---

## 8. Graph Adaptors & Views
Expand Down Expand Up @@ -1055,7 +1094,7 @@ These items block migration for the largest number of BGL users:
| **PageRank** | Algorithm | Low | Widely used iterative algorithm |
| **DIMACS read/write** | I/O | Low | Required for max-flow benchmark suites |

> **Done since the previous revision of this plan:** `filtered_graph` adaptor, DOT/GraphML/JSON I/O, Erdős-Rényi / Barabási-Albert / 2D grid / path generators, `kosaraju` + `tarjan_scc`, `afforest`, library-shipped BGL adaptor (`include/graph/adaptors/bgl/`).
> **Done since the previous revision of this plan:** `filtered_graph` adaptor, DOT/GraphML/JSON I/O, Erdős-Rényi / Barabási-Albert / 2D grid / path generators, `kosaraju` + `tarjan_scc`, `afforest`, library-shipped BGL adaptor (`include/graph/adaptors/bgl/`), composable visitor toolkit (`visitor_factory.hpp`: `make_visitor`, single-event adaptors, `predecessor_recorder`, `distance_recorder`, `time_stamper`), `valid_visitor` strict concept with `static_assert` diagnostics in BFS/DFS/Dijkstra/Bellman-Ford.

### Phase 2: Common Algorithm Coverage

Expand Down Expand Up @@ -1085,7 +1124,7 @@ These items block migration for the largest number of BGL users:
| **Max Cardinality Matching** | Algorithm | Medium | Bipartite matching |
| **Layout algorithms** | Algorithm | Medium | Graph visualization |
| **Small World / PLOD generators** | Generator | Low | Synthetic graph generation |
| **Lambda visitor composition** | API | Low | `make_visitor(on_discover_vertex([&](...){...}), ...)` |
| ~~**Lambda visitor composition**~~ | ~~API~~ | ~~Low~~ | ✅ Done — `visitor_factory.hpp`: `make_visitor`, single-event adaptors, `predecessor_recorder`, `distance_recorder`, `time_stamper` |
| **BGL compatibility header** | Migration | Medium | `graph_traits` shim + name aliases for gradual migration |

### Phase 4: Ecosystem & Tooling
Expand Down
3 changes: 3 additions & 0 deletions include/graph/algorithm/bellman_ford_shortest_paths.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ requires distance_fn_for<DistanceFn, G> && //
Compare&& compare = less<distance_fn_value_t<DistanceFn, G>>(),
Combine&& combine = plus<distance_fn_value_t<DistanceFn, G>>()) {
using graph_type = std::remove_reference_t<G>;
static_assert(valid_visitor<graph_type, Visitor>,
"Visitor has no recognized on_* callbacks. Check for a misspelled event name "
"(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks.");
using id_type = vertex_id_t<graph_type>;
using DistanceValue = distance_fn_value_t<DistanceFn, G>;
using weight_type = invoke_result_t<WF, const graph_type&, edge_t<graph_type>>;
Expand Down
3 changes: 3 additions & 0 deletions include/graph/algorithm/breadth_first_search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ void breadth_first_search(G&& g, // graph
const Sources& sources,
Visitor&& visitor = empty_visitor(),
const Alloc& alloc = Alloc()) {
static_assert(valid_visitor<G, Visitor>,
"Visitor has no recognized on_* callbacks. Check for a misspelled event name "
"(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks.");
using id_type = vertex_id_t<G>;

// Initialize BFS data structures
Expand Down
3 changes: 3 additions & 0 deletions include/graph/algorithm/depth_first_search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ void depth_first_search(G&& g, // graph
const vertex_id_t<G>& start_vertex_id, // starting vertex_id
Visitor&& visitor = empty_visitor(),
const Alloc& alloc = Alloc()) {
static_assert(valid_visitor<G, Visitor>,
"Visitor has no recognized on_* callbacks. Check for a misspelled event name "
"(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks.");
using id_type = vertex_id_t<G>;

// Vertex color states for DFS
Expand Down
3 changes: 3 additions & 0 deletions include/graph/algorithm/dijkstra_shortest_paths.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ constexpr void dijkstra_shortest_paths(
Heap /*heap_tag*/ = Heap{},
const Alloc& alloc = Alloc()) {
using graph_type = std::remove_reference_t<G>;
static_assert(valid_visitor<graph_type, Visitor>,
"Visitor has no recognized on_* callbacks. Check for a misspelled event name "
"(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks.");
using id_type = vertex_id_t<graph_type>;
using pred_id_type = predecessor_fn_value_t<PredecessorFn, G>;
using distance_type = distance_fn_value_t<DistanceFn, G>;
Expand Down
30 changes: 30 additions & 0 deletions include/graph/algorithm/traversal_common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
#pragma once

#include <algorithm>
#include <concepts>
#include <numeric>
#include <type_traits>
#include <graph/detail/graph_using.hpp>
#include <graph/graph_concepts.hpp>
#include <graph/adj_list/vertex_property_map.hpp>
Expand Down Expand Up @@ -327,6 +329,34 @@ concept has_on_finish_edge = requires(Visitor& v, const G& g, const edge_t<G>& e
/// Empty visitor type for algorithms that don't require custom callbacks
struct empty_visitor {};

//
// Aggregate / strict visitor concepts
//

/// Concept satisfied when a visitor handles at least one recognized traversal event.
/// This is the disjunction of every has_on_* visitor concept. It is used by valid_visitor
/// to distinguish a deliberately empty visitor from one whose callbacks were misnamed.
template <class G, class Visitor>
concept has_any_visitor_event = //
has_on_initialize_vertex<G, Visitor> || has_on_initialize_vertex_id<G, Visitor> || //
has_on_discover_vertex<G, Visitor> || has_on_discover_vertex_id<G, Visitor> || //
has_on_examine_vertex<G, Visitor> || has_on_examine_vertex_id<G, Visitor> || //
has_on_finish_vertex<G, Visitor> || has_on_finish_vertex_id<G, Visitor> || //
has_on_start_vertex<G, Visitor> || has_on_start_vertex_id<G, Visitor> || //
has_on_examine_edge<G, Visitor> || has_on_edge_relaxed<G, Visitor> || //
has_on_edge_not_relaxed<G, Visitor> || has_on_edge_minimized<G, Visitor> || //
has_on_edge_not_minimized<G, Visitor> || has_on_tree_edge<G, Visitor> || //
has_on_back_edge<G, Visitor> || has_on_forward_or_cross_edge<G, Visitor> || //
has_on_finish_edge<G, Visitor>;

/// Strict visitor concept: a type is a valid visitor for graph G if it is the empty_visitor
/// sentinel or it provides at least one recognized on_* callback. This catches the common
/// mistake of misspelling a callback name (e.g. on_discover_vertx), which would otherwise be
/// silently ignored because each event is detected independently via the has_on_* concepts.
template <class G, class Visitor>
concept valid_visitor = std::same_as<std::remove_cvref_t<Visitor>, empty_visitor> || //
has_any_visitor_event<G, Visitor>;

/**
* @brief A null range type for optional predecessor tracking in shortest path algorithms.
*
Expand Down
Loading
Loading