Add static network
🧪 Test / test (push) Successful in 6m4s

This commit is contained in:
2026-05-08 20:00:15 +02:00
parent 3c683c821d
commit 2bca2a7554
15 changed files with 1879 additions and 18 deletions
+175
View File
@@ -0,0 +1,175 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "node.hpp"
#include "port.hpp"
#include "traits.hpp"
#include <array>
#include <atomic>
#include <iostream>
#include <memory>
#include <thread>
#include <tuple>
#include <utility>
namespace kpn {
namespace detail {
// Produces std::tuple<T, T, ..., T> with N repetitions — used so that
// Network::connect can do its normal return_tuple type-check against FanoutNode.
template<typename T, std::size_t N, typename Seq = std::make_index_sequence<N>>
struct repeat_tuple;
template<typename T, std::size_t N, std::size_t... Is>
struct repeat_tuple<T, N, std::index_sequence<Is...>> {
template<std::size_t> using always_T = T;
using type = std::tuple<always_T<Is>...>;
};
template<typename T, std::size_t N>
using repeat_tuple_t = typename repeat_tuple<T, N>::type;
} // namespace detail
// ── FanoutNode ────────────────────────────────────────────────────────────────
//
// Reads one item from its single input channel and pushes a copy to each of
// N output channels. All N downstream nodes receive every item.
//
// Usage:
// auto fan = make_fanout<Image, 2>(/*capacity=*/8);
// net.connect("src", src.output<0>(), "fan", fan.input<0>())
// .connect("fan", fan.output<0>(), "nodeA", nodeA.input<0>())
// .connect("fan", fan.output<1>(), "nodeB", nodeB.input<0>())
template<typename T, std::size_t N>
class FanoutNode : public INode {
public:
using args_tuple = std::tuple<T>;
using return_tuple = detail::repeat_tuple_t<T, N>;
using return_raw = return_tuple;
static constexpr std::size_t input_count = 1;
static constexpr std::size_t output_count = N;
explicit FanoutNode(std::size_t fifo_capacity = 5)
: fifo_capacity_(fifo_capacity)
{
input_ch_ = std::make_shared<Channel<T>>(fifo_capacity);
}
~FanoutNode() override { stop(); }
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
input_ch_->enable();
stop_flag_.store(false, std::memory_order_relaxed);
thread_ = std::jthread([this](std::stop_token) { run_loop(); });
}
void stop() override {
stop_flag_.store(true, std::memory_order_relaxed);
input_ch_->disable();
if (thread_.joinable()) thread_.request_stop(), thread_.join();
}
bool running() const override {
return thread_.joinable() && !stop_flag_.load(std::memory_order_relaxed);
}
void set_name(std::string name) override { name_ = std::move(name); }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0};
}
// ── Port access ───────────────────────────────────────────────────────────
template<std::size_t I = 0>
InputPort<FanoutNode, I> input() {
static_assert(I == 0, "FanoutNode has exactly one input");
return {*this};
}
template<std::size_t I>
OutputPort<FanoutNode, I> output() {
static_assert(I < N, "FanoutNode output index out of range");
return {*this};
}
// ── Internal channel accessors (called by Network::connect) ───────────────
template<std::size_t I>
Channel<T>& input_channel() {
static_assert(I == 0);
return *input_ch_;
}
template<std::size_t I>
void set_input_channel(std::shared_ptr<Channel<T>> ch) {
static_assert(I == 0);
input_ch_ = std::move(ch);
}
template<std::size_t I>
void set_output_channel(Channel<T>* ch) {
static_assert(I < N);
out_channels_[I] = ch;
}
private:
void run_loop() {
while (!stop_flag_.load(std::memory_order_relaxed)) {
try {
auto t0 = clock_t::now();
T val = input_ch_->pop();
auto t1 = clock_t::now();
auto cpu0 = NodeStats::cpu_now();
for (std::size_t i = 0; i < N; ++i) {
if (out_channels_[i])
out_channels_[i]->push(val);
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
} catch (const ChannelClosedError&) {
break;
} catch (const ChannelOverflowError& e) {
std::cerr << "[kpn] fanout overflow: " << e.what() << "\n";
}
}
}
std::string name_;
std::size_t fifo_capacity_;
std::shared_ptr<Channel<T>> input_ch_;
std::array<Channel<T>*, N> out_channels_{};
std::atomic<bool> stop_flag_{false};
std::jthread thread_;
NodeStats stats_;
};
// ── Factory ───────────────────────────────────────────────────────────────────
template<typename T, std::size_t N>
FanoutNode<T, N> make_fanout(std::size_t fifo_capacity = 5) {
return FanoutNode<T, N>(fifo_capacity);
}
} // namespace kpn
+2
View File
@@ -6,5 +6,7 @@
#include "channel.hpp"
#include "port.hpp"
#include "node.hpp"
#include "fanout.hpp"
#include "static_network.hpp"
#include "main_thread_node.hpp"
#include "network.hpp"
+9
View File
@@ -12,6 +12,7 @@
#include <iomanip>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
@@ -87,6 +88,13 @@ public:
if (!src) throw NetworkBuildError("node '" + src_name + "' type mismatch");
if (!dst) throw NetworkBuildError("node '" + dst_name + "' type mismatch");
auto port_key = std::make_pair(src_name, SrcIdx);
if (connected_outputs_.count(port_key))
throw NetworkBuildError(
"connect: output port '" + src_name + "':" + std::to_string(SrcIdx)
+ " is already connected — use make_fanout<T, N> for fan-out");
connected_outputs_.insert(port_key);
auto& in_ch = dst->template input_channel<DstIdx>();
src->template set_output_channel<SrcIdx>(&in_ch);
@@ -321,6 +329,7 @@ private:
std::vector<std::string> topo_;
std::map<std::string, std::string> exposed_inputs_;
std::map<std::string, std::string> exposed_outputs_;
std::set<std::pair<std::string, std::size_t>> connected_outputs_;
std::vector<std::unique_ptr<IChannelProbe>> channel_probes_;
ErrorHandler error_handler_;
DiagnosticsHandler diag_handler_;
+39 -18
View File
@@ -37,18 +37,27 @@ struct INode {
// InputNames — optional kpn::in<"a","b"> tag type (at most one)
// OutputNames — optional kpn::out<"x","y"> tag type (at most one)
template<auto Func, typename InputTag = in<>, typename OutputTag = out<>>
template<auto Func,
typename InputTag = in<>,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class Node;
// Specialisation that unpacks the in<>/out<> tag packs
template<auto Func, fixed_string... InNames, fixed_string... OutNames>
class Node<Func, in<InNames...>, out<OutNames...>> : public INode {
template<auto Func, fixed_string... InNames, fixed_string... OutNames,
fixed_string Label, std::size_t UniqueTag>
class Node<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag> : public INode {
public:
using F = decltype(Func);
using args_tuple = args_t<F>;
using return_raw = return_t<F>;
using return_tuple = normalised_return_t<return_raw>;
// Identity accessors — used by StaticNetwork for diagnostics and type-level uniqueness
static constexpr std::string_view label() { return Label.view(); }
static constexpr std::size_t unique_tag = UniqueTag;
static constexpr std::size_t input_count = arity_v<F>;
static constexpr std::size_t output_count = std::tuple_size_v<return_tuple>;
@@ -289,17 +298,25 @@ private:
// MyFunctor obj(...);
// auto node = make_node(obj, in<"x">{}, out<"y">{}, capacity);
template<typename Obj, typename InputTag = in<>, typename OutputTag = out<>>
template<typename Obj,
typename InputTag = in<>,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class ObjectNode;
template<typename Obj, fixed_string... InNames, fixed_string... OutNames>
class ObjectNode<Obj, in<InNames...>, out<OutNames...>> : public INode {
template<typename Obj, fixed_string... InNames, fixed_string... OutNames,
fixed_string Label, std::size_t UniqueTag>
class ObjectNode<Obj, in<InNames...>, out<OutNames...>, Label, UniqueTag> : public INode {
public:
using F = decltype(&Obj::operator());
using args_tuple = args_t<F>;
using return_raw = return_t<F>;
using return_tuple = normalised_return_t<return_raw>;
static constexpr std::string_view label() { return Label.view(); }
static constexpr std::size_t unique_tag = UniqueTag;
static constexpr std::size_t input_count = arity_v<F>;
static constexpr std::size_t output_count = std::tuple_size_v<return_tuple>;
@@ -534,32 +551,36 @@ auto make_node(Obj& obj, in<InNames...>, out<OutNames...>, std::size_t fifo_capa
//
// Usage:
// make_node<func>(capacity)
// make_node<func, "label">(capacity)
// make_node<func, "label", 1>(capacity) // UniqueTag=1
// make_node<func>(in<"a","b">{}, capacity)
// make_node<func>(out<"x">{}, capacity)
// make_node<func>(in<"a","b">{}, out<"x">{}, capacity)
// make_node<func, "label">(in<"a","b">{}, out<"x">{}, capacity)
// No names
template<auto Func>
// No port names
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0>
auto make_node(std::size_t fifo_capacity = 5) {
return Node<Func, in<>, out<>>(fifo_capacity);
return Node<Func, in<>, out<>, Label, UniqueTag>(fifo_capacity);
}
// in<> only
template<auto Func, fixed_string... InNames>
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... InNames>
auto make_node(in<InNames...>, std::size_t fifo_capacity = 5) {
return Node<Func, in<InNames...>, out<>>(fifo_capacity);
return Node<Func, in<InNames...>, out<>, Label, UniqueTag>(fifo_capacity);
}
// out<> only (no input names)
template<auto Func, fixed_string... OutNames>
// out<> only
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... OutNames>
auto make_node(out<OutNames...>, std::size_t fifo_capacity = 5) {
return Node<Func, in<>, out<OutNames...>>(fifo_capacity);
return Node<Func, in<>, out<OutNames...>, Label, UniqueTag>(fifo_capacity);
}
// in<> and out<>
template<auto Func, fixed_string... InNames, fixed_string... OutNames>
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... InNames, fixed_string... OutNames>
auto make_node(in<InNames...>, out<OutNames...>, std::size_t fifo_capacity = 5) {
return Node<Func, in<InNames...>, out<OutNames...>>(fifo_capacity);
return Node<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag>(fifo_capacity);
}
} // namespace kpn
+237
View File
@@ -0,0 +1,237 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "fanout.hpp"
#include "node.hpp"
#include "port.hpp"
#include "tmp/fanout_groups.hpp"
#include "tmp/topo_sort.hpp"
#include <iostream>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>
namespace kpn {
// ── Edge descriptor ───────────────────────────────────────────────────────────
//
// Carries compile-time type info and runtime references to the two endpoints.
// Constructed by the edge() factory; consumed by make_network().
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
struct Edge {
using src_node_t = SrcNode;
using dst_node_t = DstNode;
static constexpr std::size_t src_idx = SrcIdx;
static constexpr std::size_t dst_idx = DstIdx;
SrcNode& src;
DstNode& dst;
};
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
auto edge(OutputPort<SrcNode, SrcIdx>, InputPort<DstNode, DstIdx> in)
-> Edge<SrcNode, SrcIdx, DstNode, DstIdx>; // deduction only; defined below
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
Edge<SrcNode, SrcIdx, DstNode, DstIdx>
edge(OutputPort<SrcNode, SrcIdx> out, InputPort<DstNode, DstIdx> in) {
return {out.node, in.node};
}
// ── StaticNetwork ─────────────────────────────────────────────────────────────
// node_label<NodeT>: returns Label NTTP as string_view if present, else empty.
template<typename NodeT, typename = void>
struct node_label_helper {
static constexpr std::string_view value = "";
};
template<typename NodeT>
struct node_label_helper<NodeT, std::void_t<decltype(NodeT::label())>> {
static constexpr std::string_view value = NodeT::label();
};
template<typename NodeT>
inline constexpr std::string_view node_label_v = node_label_helper<NodeT>::value;
// node_display_name<NodeT, UniqueTag>: Label if non-empty, else "node[UniqueTag]"
// Returned as std::string at runtime (called once at StaticNetwork construction).
template<typename NodeT>
std::string node_display_name() {
constexpr std::string_view lbl = node_label_v<NodeT>;
if constexpr (!lbl.empty()) {
return std::string(lbl);
} else if constexpr (requires { NodeT::unique_tag; }) {
return "node[" + std::to_string(NodeT::unique_tag) + "]";
} else {
return "node[?]";
}
}
template<typename FanoutStorage, typename TopoNodeList>
class StaticNetwork : public INode {
public:
// FanoutStorage = std::tuple<FanoutNode<T0,N0>, ...> (owned, heap-allocated)
// TopoNodeList = tmp::TypeList<NodeA, NodeB, ...> sources-first
StaticNetwork(std::unique_ptr<FanoutStorage> fanouts,
std::vector<INode*> user_nodes_topo,
std::vector<INode*> fanout_ptrs,
std::vector<std::string> user_node_names)
: fanouts_(std::move(fanouts))
, user_nodes_topo_(std::move(user_nodes_topo))
, fanout_nodes_ptr_(std::move(fanout_ptrs))
, user_node_names_(std::move(user_node_names))
{}
~StaticNetwork() override { stop(); }
void start() override {
stop_flag_ = false;
for (auto* n : user_nodes_topo_) n->start();
for (auto* n : fanout_nodes_ptr_) n->start();
}
void stop() override {
stop_flag_ = true;
for (auto it = fanout_nodes_ptr_.rbegin(); it != fanout_nodes_ptr_.rend(); ++it)
(*it)->stop();
for (auto it = user_nodes_topo_.rbegin(); it != user_nodes_topo_.rend(); ++it)
(*it)->stop();
}
bool running() const override { return !stop_flag_; }
void set_name(std::string name) override { name_ = std::move(name); }
const NodeStats& stats() const override { static NodeStats dummy; return dummy; }
NodeSnapshot node_snapshot(const std::string& n, double) const override {
return {n, 0, 0, 0, 0, 0, 0, 0};
}
// Print diagnostics using compile-time node labels
void print_diagnostics(std::ostream& os = std::cerr) const {
os << "\n┌─ KPN++ StaticNetwork diagnostics ─────────────────────────────\n";
for (std::size_t i = 0; i < user_nodes_topo_.size(); ++i) {
auto snap = user_nodes_topo_[i]->node_snapshot(user_node_names_[i], 0.0);
os << "" << snap.name
<< " frames=" << snap.frames_processed
<< " ema=" << snap.ema_exec_ms << "ms\n";
}
os << "└────────────────────────────────────────────────────────────────\n";
}
FanoutStorage& fanouts_storage() { return *fanouts_; }
private:
std::string name_;
bool stop_flag_{false};
std::unique_ptr<FanoutStorage> fanouts_;
std::vector<INode*> user_nodes_topo_;
std::vector<INode*> fanout_nodes_ptr_;
std::vector<std::string> user_node_names_; // parallel to user_nodes_topo_
};
// ── make_network ──────────────────────────────────────────────────────────────
template<typename... Edges>
auto make_network(Edges&&... edges) {
// 1. Expand edges — detect fan-outs, splice FanoutNodes
using FanoutSto = tmp::fanout_storage_t<std::decay_t<Edges>...>;
using ExpandedEdges = tmp::expanded_edges_t<std::decay_t<Edges>...>;
// 2. Duplicate-tag check — fires before cycle check for a cleaner error message
using UserNodes = typename tmp::all_node_types<tmp::TypeList<std::decay_t<Edges>...>>::type;
static_assert(!tmp::has_duplicate_tags_v<std::decay_t<Edges>...>,
"make_network: two nodes have the same (Func, UniqueTag) — they are "
"indistinguishable as graph vertices. Add a UniqueTag: "
"make_node<func, \"label\", 1>(capacity)");
// 3. Cycle check
using Topo = tmp::topo_sort<ExpandedEdges>;
constexpr bool has_cycle = Topo::has_cycle;
static_assert(!has_cycle,
"make_network: graph contains a directed cycle");
// 4. Construct owned fanout storage on the heap (FanoutNode has jthread — not moveable)
auto fanout_storage = std::make_unique<FanoutSto>();
// 5. Collect unique user node pointers + their display names, in edge-declaration order
std::vector<INode*> user_node_ptrs;
std::vector<std::string> user_node_names;
auto collect = [&](auto& e) {
using SrcT = std::decay_t<decltype(e.src)>;
using DstT = std::decay_t<decltype(e.dst)>;
auto* s = static_cast<INode*>(&e.src);
auto* d = static_cast<INode*>(&e.dst);
if (std::find(user_node_ptrs.begin(), user_node_ptrs.end(), s) == user_node_ptrs.end()) {
user_node_ptrs.push_back(s);
user_node_names.push_back(node_display_name<SrcT>());
}
if (std::find(user_node_ptrs.begin(), user_node_ptrs.end(), d) == user_node_ptrs.end()) {
user_node_ptrs.push_back(d);
user_node_names.push_back(node_display_name<DstT>());
}
};
(collect(edges), ...);
// 5. Wire all expanded SimpleEdges.
// find_node<NodeT>: searches fanout storage then user edge pack, returns NodeT*.
// Uses if constexpr in a fold so mismatched types never reach assignment.
auto find_node = [&]<typename NodeT>() -> NodeT* {
NodeT* ptr = nullptr;
std::apply([&](auto&... fn) {
([&](auto& node) {
if constexpr (std::is_same_v<std::decay_t<decltype(node)>, NodeT>)
if (!ptr) ptr = &node;
}(fn), ...);
}, *fanout_storage);
if (!ptr) {
([&](auto& e) {
if (!ptr) {
if constexpr (std::is_same_v<std::decay_t<decltype(e.src)>, NodeT>)
ptr = &e.src;
else if constexpr (std::is_same_v<std::decay_t<decltype(e.dst)>, NodeT>)
ptr = &e.dst;
}
}(edges), ...);
}
return ptr;
};
auto wire_one = [&]<typename SE>(SE) {
using SrcNode = typename SE::src_node_t;
using DstNode = typename SE::dst_node_t;
constexpr std::size_t SrcIdx = SE::src_idx;
constexpr std::size_t DstIdx = SE::dst_idx;
auto* src = find_node.template operator()<SrcNode>();
auto* dst = find_node.template operator()<DstNode>();
if (src && dst)
src->template set_output_channel<SrcIdx>(
&dst->template input_channel<DstIdx>());
};
[&]<typename... SEs>(tmp::TypeList<SEs...>) {
(wire_one(SEs{}), ...);
}(ExpandedEdges{});
// 6. Collect fanout node pointers
std::vector<INode*> fanout_ptrs;
std::apply([&](auto&... fn) {
(fanout_ptrs.push_back(static_cast<INode*>(&fn)), ...);
}, *fanout_storage);
// 7. Construct and return the StaticNetwork
using Net = StaticNetwork<FanoutSto, typename Topo::topo>;
return Net(std::move(fanout_storage),
std::move(user_node_ptrs),
std::move(fanout_ptrs),
std::move(user_node_names));
}
} // namespace kpn
+358
View File
@@ -0,0 +1,358 @@
#pragma once
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <utility>
// Metafunctions that scan a pack of Edge<> types, group edges by source port,
// auto-insert FanoutNode<T,N> where N>1, and produce an expanded edge list
// plus a tuple type for the owned fanout nodes.
//
// Key types produced:
// expanded_edges_t<Edges...> — type list of SimpleEdge after fanout insertion
// fanout_storage_t<Edges...> — std::tuple<FanoutNode<T0,N0>, ...> to own
namespace kpn::tmp {
// ── Type list ─────────────────────────────────────────────────────────────────
template<typename... Ts>
struct TypeList {};
template<typename List, typename T>
struct append;
template<typename... Ts, typename T>
struct append<TypeList<Ts...>, T> { using type = TypeList<Ts..., T>; };
template<typename List, typename T>
using append_t = typename append<List, T>::type;
template<typename A, typename B>
struct concat;
template<typename... As, typename... Bs>
struct concat<TypeList<As...>, TypeList<Bs...>> { using type = TypeList<As..., Bs...>; };
template<typename A, typename B>
using concat_t = typename concat<A, B>::type;
// ── Source port identity (used as compile-time map key) ───────────────────────
// Two edges share a source port when their SrcNode type AND SrcIdx are identical.
template<typename SrcNode, std::size_t SrcIdx>
struct SrcPort {};
template<typename Edge>
using src_port_of = SrcPort<typename Edge::src_node_t, Edge::src_idx>;
// ── Count occurrences of a SrcPort in an edge list ───────────────────────────
template<typename Port, typename EdgeList>
struct count_port;
template<typename Port>
struct count_port<Port, TypeList<>> : std::integral_constant<std::size_t, 0> {};
template<typename Port, typename Head, typename... Tail>
struct count_port<Port, TypeList<Head, Tail...>>
: std::integral_constant<std::size_t,
(std::is_same_v<Port, src_port_of<Head>> ? 1 : 0)
+ count_port<Port, TypeList<Tail...>>::value> {};
// ── Collect all destination (DstNode&, DstIdx) for a given SrcPort ────────────
// A destination descriptor — just type tags, no references (references go in
// the runtime SimpleEdge structs produced after wiring).
template<typename DstNode, std::size_t DstIdx>
struct DstDesc {};
template<typename Port, typename EdgeList>
struct collect_dsts;
template<typename Port>
struct collect_dsts<Port, TypeList<>> { using type = TypeList<>; };
template<typename Port, typename Head, typename... Tail>
struct collect_dsts<Port, TypeList<Head, Tail...>> {
using rest = typename collect_dsts<Port, TypeList<Tail...>>::type;
using type = std::conditional_t<
std::is_same_v<Port, src_port_of<Head>>,
append_t<rest, DstDesc<typename Head::dst_node_t, Head::dst_idx>>,
rest>;
};
// ── SimpleEdge: a resolved edge after fanout expansion ────────────────────────
//
// At runtime, StaticNetwork wires edges using SimpleEdge descriptors.
// Each SimpleEdge is just a pair of (SrcNode&, SrcIdx, DstNode&, DstIdx) stored
// as a type — the actual references come from the node tuple at wire time.
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
struct SimpleEdge {
using src_node_t = SrcNode;
using dst_node_t = DstNode;
static constexpr std::size_t src_idx = SrcIdx;
static constexpr std::size_t dst_idx = DstIdx;
};
// ── FanoutPlaceholder: a fanout node that will be owned by StaticNetwork ───────
template<typename T, std::size_t N, std::size_t FanoutId>
struct FanoutPlaceholder {
using value_type = T;
static constexpr std::size_t fan_n = N;
static constexpr std::size_t fan_id = FanoutId;
};
// ── expand_edge: for one original edge, produce the replacement SimpleEdge(s) ──
//
// If the source port has N>1 consumers: the edge from src→fanout and the
// fanout→dst edges are synthesised elsewhere (see expand_all). Here we only
// need to emit the fanout→dst edge for this particular destination.
//
// For N==1 edges we emit the edge unchanged.
//
// This is called after the fanout node type has already been determined.
} // namespace kpn::tmp
namespace kpn {
// Forward declaration — FanoutNode is defined in fanout.hpp.
template<typename T, std::size_t N> class FanoutNode;
} // namespace kpn
namespace kpn::tmp {
// ── Master expansion: iterate edges, build expanded list + storage tuple ───────
//
// Strategy:
// 1. First pass: for each unique SrcPort with N>1, record a FanoutPlaceholder.
// 2. Second pass: rewrite each edge.
// - N==1 edges become a single SimpleEdge unchanged.
// - N>1 edges: on first encounter emit src→fanout SimpleEdge + N fanout→dst
// SimpleEdges; on subsequent encounters for the same src port emit nothing
// (already handled).
//
// To implement "first encounter" tracking we carry a list of already-processed
// SrcPorts through the fold.
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, // TypeList<FanoutPlaceholder<...>>
typename EdgeList, // TypeList<SimpleEdge<...>> — accumulated output
typename RemainingEdges> // TypeList<original edges> still to process
struct expand_impl;
// Base case — no more edges
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, typename EdgeList>
struct expand_impl<ProcessedPorts, NextFanoutId, FanoutList, EdgeList, TypeList<>> {
using fanouts = FanoutList;
using edges = EdgeList;
};
// Helper: is Port in ProcessedPorts?
template<typename Port, typename Processed>
struct already_processed : std::false_type {};
template<typename Port, typename Head, typename... Tail>
struct already_processed<Port, TypeList<Head, Tail...>>
: std::conditional_t<std::is_same_v<Port, Head>,
std::true_type,
already_processed<Port, TypeList<Tail...>>> {};
// Helper: given DstDesc list + FanoutPlaceholder id, produce SimpleEdge list
// FanoutNode<T,N>::output<I> → DstNode::input<DstIdx>
template<std::size_t FanoutId, typename T, std::size_t N,
typename DstDescList, std::size_t I = 0>
struct fanout_to_dst_edges;
template<std::size_t FanoutId, typename T, std::size_t N, std::size_t I>
struct fanout_to_dst_edges<FanoutId, T, N, TypeList<>, I> {
using type = TypeList<>;
};
template<std::size_t FanoutId, typename T, std::size_t N,
typename DstNodeT, std::size_t DstI, typename... DstTail, std::size_t I>
struct fanout_to_dst_edges<FanoutId, T, N,
TypeList<DstDesc<DstNodeT, DstI>, DstTail...>, I> {
using head_edge = SimpleEdge<kpn::FanoutNode<T, N>, I, DstNodeT, DstI>;
using rest = typename fanout_to_dst_edges<FanoutId, T, N,
TypeList<DstTail...>, I+1>::type;
using type = append_t<rest, head_edge>;
};
// Recursive case — process head edge
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, typename EdgeList,
typename Head, typename... Tail>
struct expand_impl<ProcessedPorts, NextFanoutId, FanoutList, EdgeList,
TypeList<Head, Tail...>> {
using AllEdges = TypeList<Head, Tail...>;
using Port = src_port_of<Head>;
using SrcNode = typename Head::src_node_t;
using T = std::tuple_element_t<Head::src_idx,
typename SrcNode::return_tuple>;
static constexpr std::size_t N = count_port<Port, AllEdges>::value
+ count_port<Port, EdgeList>::value
// recount against full original list approximation:
// simpler: recount in remaining + already done
;
// Recount properly against the complete original edge list is not possible here
// without passing it along. Instead we pre-compute N before entering the fold.
// See expand_all below which pre-computes per-port counts.
//
// This struct is not used directly — expand_all drives the logic with pre-computed N.
};
// ── expand_all: top-level entry point ─────────────────────────────────────────
//
// Pre-computes per-source-port counts, then runs a fold that processes edges
// one by one.
// Port count map entry
template<typename Port, std::size_t Count>
struct PortCount {};
// Build port count list from full edge list
template<typename AllEdges, typename UniquePortsSeen>
struct build_port_counts;
template<typename AllEdges>
struct build_port_counts<AllEdges, TypeList<>> {
using type = TypeList<>;
};
template<typename AllEdges, typename HeadPort, typename... TailPorts>
struct build_port_counts<AllEdges, TypeList<HeadPort, TailPorts...>> {
static constexpr std::size_t cnt = count_port<HeadPort, AllEdges>::value;
using rest = typename build_port_counts<AllEdges, TypeList<TailPorts...>>::type;
using type = append_t<rest, PortCount<HeadPort, cnt>>;
};
// Collect unique source ports from edge list
template<typename EdgeList, typename SeenSoFar = TypeList<>>
struct unique_src_ports;
template<typename SeenSoFar>
struct unique_src_ports<TypeList<>, SeenSoFar> { using type = SeenSoFar; };
template<typename Head, typename... Tail, typename SeenSoFar>
struct unique_src_ports<TypeList<Head, Tail...>, SeenSoFar> {
using Port = src_port_of<Head>;
using next_seen = std::conditional_t<
already_processed<Port, SeenSoFar>::value,
SeenSoFar,
append_t<SeenSoFar, Port>>;
using type = typename unique_src_ports<TypeList<Tail...>, next_seen>::type;
};
// Look up count for a port
template<typename Port, typename CountList>
struct lookup_count : std::integral_constant<std::size_t, 1> {};
template<typename Port, std::size_t N, typename... Rest>
struct lookup_count<Port, TypeList<PortCount<Port, N>, Rest...>>
: std::integral_constant<std::size_t, N> {};
template<typename Port, typename Head, typename... Rest>
struct lookup_count<Port, TypeList<Head, Rest...>>
: lookup_count<Port, TypeList<Rest...>> {};
// Fold state for the wiring pass
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, typename EdgeList>
struct FoldState {
using processed = ProcessedPorts;
static constexpr std::size_t next_id = NextFanoutId;
using fanouts = FanoutList;
using edges = EdgeList;
};
// Process one edge given pre-computed port counts
template<typename State, typename Edge, typename CountList, typename AllEdges>
struct process_edge {
using Port = src_port_of<Edge>;
using SrcNode = typename Edge::src_node_t;
using T = std::tuple_element_t<Edge::src_idx, typename SrcNode::return_tuple>;
static constexpr std::size_t N = lookup_count<Port, CountList>::value;
// N==1: pass through unchanged
using passthrough_edges = append_t<typename State::edges,
SimpleEdge<SrcNode, Edge::src_idx,
typename Edge::dst_node_t, Edge::dst_idx>>;
// N>1, first encounter: emit src→fanout + all fanout→dst edges
using DstDescs = typename collect_dsts<Port, AllEdges>::type;
static constexpr std::size_t fid = State::next_id;
using fanout_type = FanoutPlaceholder<T, N, fid>;
using src_to_fan = SimpleEdge<SrcNode, Edge::src_idx, FanoutNode<T, N>, 0>;
using fan_to_dsts = typename fanout_to_dst_edges<fid, T, N, DstDescs>::type;
using fanout_edges = concat_t<append_t<typename State::edges, src_to_fan>, fan_to_dsts>;
using new_fanouts = append_t<typename State::fanouts, fanout_type>;
static constexpr bool seen = already_processed<Port, typename State::processed>::value;
using type = std::conditional_t<
(N == 1),
FoldState<typename State::processed, State::next_id,
typename State::fanouts, passthrough_edges>,
std::conditional_t<
!seen,
FoldState<append_t<typename State::processed, Port>,
State::next_id + 1,
new_fanouts, fanout_edges>,
// already processed — skip (fanout edges already emitted)
State>>;
};
// Fold over all edges
template<typename State, typename EdgeList, typename CountList, typename AllEdges>
struct fold_edges;
template<typename State, typename CountList, typename AllEdges>
struct fold_edges<State, TypeList<>, CountList, AllEdges> { using type = State; };
template<typename State, typename Head, typename... Tail,
typename CountList, typename AllEdges>
struct fold_edges<State, TypeList<Head, Tail...>, CountList, AllEdges> {
using next = typename process_edge<State, Head, CountList, AllEdges>::type;
using type = typename fold_edges<next, TypeList<Tail...>, CountList, AllEdges>::type;
};
// ── Public interface ───────────────────────────────────────────────────────────
template<typename... Edges>
struct expand_all {
using AllEdges = TypeList<Edges...>;
using UniquePorts = typename unique_src_ports<AllEdges>::type;
using CountList = typename build_port_counts<AllEdges, UniquePorts>::type;
using InitState = FoldState<TypeList<>, 0, TypeList<>, TypeList<>>;
using FinalState = typename fold_edges<InitState, AllEdges, CountList, AllEdges>::type;
using fanout_placeholders = typename FinalState::fanouts; // TypeList<FanoutPlaceholder<...>>
using expanded_edges = typename FinalState::edges; // TypeList<SimpleEdge<...>>
};
// Convert TypeList<FanoutPlaceholder<T,N,Id>...> to std::tuple<FanoutNode<T,N>...>
template<typename PlaceholderList>
struct to_fanout_tuple;
template<>
struct to_fanout_tuple<TypeList<>> { using type = std::tuple<>; };
template<typename T, std::size_t N, std::size_t Id, typename... Rest>
struct to_fanout_tuple<TypeList<FanoutPlaceholder<T, N, Id>, Rest...>> {
using rest = typename to_fanout_tuple<TypeList<Rest...>>::type;
// prepend FanoutNode<T,N>
template<typename Tuple> struct prepend;
template<typename... Ts> struct prepend<std::tuple<Ts...>> {
using type = std::tuple<kpn::FanoutNode<T, N>, Ts...>;
};
using type = typename prepend<rest>::type;
};
template<typename... Edges>
using fanout_storage_t = typename to_fanout_tuple<
typename expand_all<Edges...>::fanout_placeholders>::type;
template<typename... Edges>
using expanded_edges_t = typename expand_all<Edges...>::expanded_edges;
} // namespace kpn::tmp
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstddef>
#include <tuple>
#include <utility>
namespace kpn::tmp {
// repeat_tuple_t<T, N> — std::tuple<T, T, ..., T> with N elements.
// Used by FanoutNode and fanout_groups to express N homogeneous outputs.
template<typename T, std::size_t N, typename Seq = std::make_index_sequence<N>>
struct repeat_tuple;
template<typename T, std::size_t N, std::size_t... Is>
struct repeat_tuple<T, N, std::index_sequence<Is...>> {
template<std::size_t> using always_T = T;
using type = std::tuple<always_T<Is>...>;
};
template<typename T, std::size_t N>
using repeat_tuple_t = typename repeat_tuple<T, N>::type;
} // namespace kpn::tmp
+223
View File
@@ -0,0 +1,223 @@
#pragma once
#include "fanout_groups.hpp"
#include <cstddef>
#include <tuple>
#include <type_traits>
// Compile-time topological sort + cycle detection over a TypeList<SimpleEdge<...>>.
//
// Nodes are identified by type (not by index), since two different node types
// are always distinct vertices. The sort produces a TypeList of node types in
// topological order (sources first).
//
// cycle detection: if DFS revisits a node currently on the stack → static_assert.
namespace kpn::tmp {
// ── Collect all unique node types from an expanded edge list ──────────────────
template<typename EdgeList, typename Seen = TypeList<>>
struct all_node_types;
template<typename Seen>
struct all_node_types<TypeList<>, Seen> { using type = Seen; };
template<typename Head, typename... Tail, typename Seen>
struct all_node_types<TypeList<Head, Tail...>, Seen> {
using S1 = std::conditional_t<
already_processed<typename Head::src_node_t, Seen>::value,
Seen, append_t<Seen, typename Head::src_node_t>>;
using S2 = std::conditional_t<
already_processed<typename Head::dst_node_t, S1>::value,
S1, append_t<S1, typename Head::dst_node_t>>;
using type = typename all_node_types<TypeList<Tail...>, S2>::type;
};
// ── Collect successors (dst node types) of a given src node type ──────────────
template<typename NodeType, typename EdgeList>
struct successors;
template<typename NodeType>
struct successors<NodeType, TypeList<>> { using type = TypeList<>; };
template<typename NodeType, typename Head, typename... Tail>
struct successors<NodeType, TypeList<Head, Tail...>> {
using rest = typename successors<NodeType, TypeList<Tail...>>::type;
using type = std::conditional_t<
std::is_same_v<typename Head::src_node_t, NodeType>,
std::conditional_t<
already_processed<typename Head::dst_node_t, rest>::value,
rest,
append_t<rest, typename Head::dst_node_t>>,
rest>;
};
// ── DFS state ─────────────────────────────────────────────────────────────────
// Color: 0=white(unseen), 1=grey(on stack), 2=black(done)
// We track grey nodes as a TypeList to detect back-edges.
template<typename Node, typename GreyList>
struct is_grey : already_processed<Node, GreyList> {};
// DFS result: has_cycle flag + topo order (black nodes appended post-visit)
template<bool Cycle, typename TopoList>
struct DfsResult { static constexpr bool has_cycle = Cycle; using topo = TopoList; };
template<typename Node, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList>
struct dfs_node;
// Iterate successors
template<typename SuccList, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_successors;
template<typename EdgeList, typename GreyList, typename BlackList,
typename TopoList, bool CycleSoFar>
struct dfs_successors<TypeList<>, EdgeList, GreyList, BlackList, TopoList, CycleSoFar> {
static constexpr bool has_cycle = CycleSoFar;
using black = BlackList;
using topo = TopoList;
};
template<typename Head, typename... Tail, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_successors<TypeList<Head, Tail...>, EdgeList,
GreyList, BlackList, TopoList, CycleSoFar> {
// Visit Head
using visit = dfs_node<Head, EdgeList, GreyList, BlackList, TopoList>;
static constexpr bool cycle1 = CycleSoFar || visit::has_cycle;
// Continue with remaining successors using updated black/topo
using rest = dfs_successors<TypeList<Tail...>, EdgeList,
GreyList, typename visit::black,
typename visit::topo, cycle1>;
static constexpr bool has_cycle = rest::has_cycle;
using black = typename rest::black;
using topo = typename rest::topo;
};
template<typename Node, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList>
struct dfs_node {
// Already black — skip
static constexpr bool already_done = already_processed<Node, BlackList>::value;
// On stack — cycle
static constexpr bool on_stack = is_grey<Node, GreyList>::value;
// Visit successors (only if not already done / on stack)
using succs = typename successors<Node, EdgeList>::type;
using new_grey = append_t<GreyList, Node>;
using visit_succs = dfs_successors<succs, EdgeList, new_grey, BlackList, TopoList,
on_stack>;
static constexpr bool has_cycle = already_done ? false :
on_stack ? true :
visit_succs::has_cycle;
// Append node to topo after all successors (post-order = reverse topo)
using topo_after = std::conditional_t<
already_done || on_stack,
TopoList,
append_t<typename visit_succs::topo, Node>>;
using black = std::conditional_t<
already_done || on_stack,
BlackList,
append_t<typename visit_succs::black, Node>>;
using topo = topo_after;
};
// ── Top-level DFS over all nodes ──────────────────────────────────────────────
template<typename NodeList, typename EdgeList,
typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_all;
template<typename EdgeList, typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_all<TypeList<>, EdgeList, BlackList, TopoList, CycleSoFar> {
static constexpr bool has_cycle = CycleSoFar;
using topo = TopoList;
};
template<typename Head, typename... Tail, typename EdgeList,
typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_all<TypeList<Head, Tail...>, EdgeList, BlackList, TopoList, CycleSoFar> {
using visit = dfs_node<Head, EdgeList, TypeList<>, BlackList, TopoList>;
static constexpr bool cycle1 = CycleSoFar || visit::has_cycle;
using rest = dfs_all<TypeList<Tail...>, EdgeList,
typename visit::black, typename visit::topo, cycle1>;
static constexpr bool has_cycle = rest::has_cycle;
using topo = typename rest::topo;
};
// ── Public interface ───────────────────────────────────────────────────────────
// topo_sort<EdgeList>:
// ::topo — TypeList of node types, sources first (start order)
// ::has_cycle — true if a cycle was detected
template<typename EdgeList>
struct topo_sort {
using Nodes = typename all_node_types<EdgeList>::type;
using result = dfs_all<Nodes, EdgeList, TypeList<>, TypeList<>, false>;
// DFS post-order gives reverse topo; reverse the list for sources-first order.
// Reversing a TypeList:
template<typename List, typename Acc = TypeList<>>
struct reverse_list;
template<typename Acc>
struct reverse_list<TypeList<>, Acc> { using type = Acc; };
template<typename H, typename... T, typename Acc>
struct reverse_list<TypeList<H, T...>, Acc>
: reverse_list<TypeList<T...>, append_t<Acc, H>> {}; // wrong direction intentionally
// Post-order appends children before parent, so the list is already
// reverse-topo (sinks first). Reverse it to get sources first.
template<typename List, typename Acc = TypeList<>>
struct rev;
template<typename Acc>
struct rev<TypeList<>, Acc> { using type = Acc; };
template<typename H, typename... T, typename Acc>
struct rev<TypeList<H, T...>, Acc> : rev<TypeList<T...>, TypeList<H, Acc>> {
// prepend H to Acc: TypeList<H, Acc...>
};
// Simpler: just reverse by prepending
template<typename L, typename A = TypeList<>>
struct rev2 { using type = A; };
template<typename H, typename... T, typename... As>
struct rev2<TypeList<H, T...>, TypeList<As...>>
: rev2<TypeList<T...>, TypeList<H, As...>> {};
static constexpr bool has_cycle = result::has_cycle;
using topo = typename rev2<typename result::topo>::type;
};
// ── Duplicate-tag detection ───────────────────────────────────────────────────
//
// Two user nodes share a (Func, UniqueTag) pair iff they have the same type.
// has_duplicate_tags_v<Edges...> is true if any two user node types are identical.
template<typename T, typename List>
struct type_in_list : std::false_type {};
template<typename T, typename H, typename... Tail>
struct type_in_list<T, TypeList<H, Tail...>>
: std::conditional_t<std::is_same_v<T, H>,
std::true_type,
type_in_list<T, TypeList<Tail...>>> {};
template<typename NodeList>
struct has_duplicate_node_types : std::false_type {};
template<typename H, typename... Tail>
struct has_duplicate_node_types<TypeList<H, Tail...>>
: std::conditional_t<type_in_list<H, TypeList<Tail...>>::value,
std::true_type,
has_duplicate_node_types<TypeList<Tail...>>> {};
template<typename... Edges>
inline constexpr bool has_duplicate_tags_v =
has_duplicate_node_types<
typename all_node_types<TypeList<Edges...>>::type>::value;
} // namespace kpn::tmp