fsm 0.4.1
Header-only C++20 hierarchical table-driven FSM
Loading...
Searching...
No Matches
dot.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
17#ifndef FSM_DOT_HPP
18#define FSM_DOT_HPP
19
20#include "fsm/fsm.hpp"
21
22static_assert(FSM_ENABLE_INTROSPECTION,
23 "fsm: dot export requires FSM_ENABLE_INTROSPECTION");
24
25namespace fsm {
26
44template <class Traits, class W>
45void write_dot(W&& write) {
46 using M = machine<Traits>;
47 using State = typename Traits::State;
48 detail::line_buf lb;
49
50 write("digraph fsm {");
51 write(" rankdir=LR;");
52 write(" compound=true;");
53 write(" node [shape=box, style=rounded];");
54 write(" any [shape=point, xlabel=\"any\"];");
55 write(" refused [shape=octagon, style=solid];");
56
57 // States: composites as clusters containing their (transitive) children's
58 // leaf nodes; leaves as plain nodes. One nesting level of cluster per
59 // composite, emitted depth-first.
60 constexpr auto& specs = Traits::states;
61 // Emit clusters for roots first; nested composites are emitted within.
62 struct Emitter {
63 W& w;
64 detail::line_buf& lb;
65 void emit_state(State s, int indent) {
66 if (detail::is_composite(Traits::states, s)) {
67 line(indent);
68 lb.str("subgraph cluster_");
69 M::put_state_name(lb, s);
70 lb.str(" {");
71 w(lb.b);
72 line(indent + 1);
73 lb.str("label=\"");
74 M::put_state_name(lb, s);
75 lb.str("\";");
76 w(lb.b);
77 for (size_t i = 0; i < M::state_count; ++i) {
78 const auto& sp = Traits::states[i];
79 if (sp.parent.k == parent_kind::child && sp.parent.s == s)
80 emit_state(sp.state.s, indent + 1);
81 }
82 line(indent);
83 lb.str("}");
84 w(lb.b);
85 } else {
86 line(indent);
87 M::put_state_name(lb, s);
88 lb.str(";");
89 w(lb.b);
90 }
91 }
92 void line(int indent) {
93 lb.reset();
94 for (int i = 0; i < indent; ++i) lb.str(" ");
95 }
96 } em{write, lb};
97
98 for (size_t i = 0; i < M::state_count; ++i) {
99 const auto& sp = specs[i];
100 if (sp.parent.k != parent_kind::child) em.emit_state(sp.state.s, 1);
101 }
102
103 // Edge endpoints: Graphviz cannot attach an edge to a cluster by name (an
104 // unknown name would silently create a phantom node), so edges touching a
105 // composite anchor at its initial-descent leaf and clip at the cluster
106 // border via ltail/lhead (hence compound=true above).
107 auto endpoint = [&lb](State s) {
108 M::put_state_name(lb, detail::leaf_of(Traits::states, s));
109 };
110 auto cluster_attr = [&lb](const char* attr, State s) {
111 lb.str(", ");
112 lb.str(attr);
113 lb.str("=cluster_");
114 M::put_state_name(lb, s);
115 };
116
117 // Transition rows.
118 for (size_t i = 0; i < M::row_count; ++i) {
119 const auto& r = Traits::rows[i];
120 const bool from_comp =
121 r.from.k == from_kind::state && detail::is_composite(specs, r.from.s);
122 bool to_comp = false;
123 lb.reset();
124 lb.str(" ");
125 if (r.from.k == from_kind::any) lb.str("any");
126 else endpoint(r.from.s);
127 lb.str(" -> ");
128 switch (r.to.k) {
129 case target_kind::to:
130 to_comp = detail::is_composite(specs, r.to.s);
131 endpoint(r.to.s);
132 break;
134 to_comp = from_comp; // self-annotation mirrors the source
135 if (r.from.k == from_kind::any) lb.str("any");
136 else endpoint(r.from.s);
137 break;
139 default:
140 lb.str("refused");
141 break;
142 }
143 lb.str(" [label=\"");
144 M::put_event_name(lb, r.on.e);
145 if (r.when != nullptr) lb.str(" [guard]");
146 if (r.then != nullptr) lb.str(" /action");
147 if (r.to.k == target_kind::refuse_) {
148 lb.str(" refuse(");
149 M::put_reason_name(lb, r.to.r);
150 lb.str(")");
151 }
152 if (r.label != nullptr) {
153 lb.str("\\n"); // graphviz newline inside the edge label
154 lb.str(r.label);
155 }
156 lb.str("\"");
157 if (r.to.k == target_kind::internal_) lb.str(", style=dotted");
158 if (from_comp) cluster_attr("ltail", r.from.s);
159 if (to_comp)
160 cluster_attr("lhead", r.to.k == target_kind::to ? r.to.s : r.from.s);
161 lb.str("]");
162 lb.str(";");
163 write(lb.b);
164 }
165
166 // Timeout annotations: dashed self-edges labelled with duration + event.
167 for (size_t i = 0; i < M::state_count; ++i) {
168 const auto& sp = specs[i];
169 if (sp.deadline.k != timeout_kind::after_ &&
170 sp.deadline.k != timeout_kind::after_dyn_)
171 continue;
172 const bool comp = detail::is_composite(specs, sp.state.s);
173 lb.reset();
174 lb.str(" ");
175 endpoint(sp.state.s);
176 lb.str(" -> ");
177 endpoint(sp.state.s);
178 lb.str(" [style=dashed, label=\"after ");
179 if (sp.deadline.k == timeout_kind::after_dyn_) {
180 lb.str("dyn: ");
181 } else {
182 lb.u64(sp.deadline.ms);
183 lb.str("ms: ");
184 }
185 M::put_event_name(lb, sp.deadline.event);
186 lb.str("\"");
187 if (comp) {
188 cluster_attr("ltail", sp.state.s);
189 cluster_attr("lhead", sp.state.s);
190 }
191 lb.str("];");
192 write(lb.b);
193 }
194
195 write("}");
196}
197
198} // namespace fsm
199
200#endif // FSM_DOT_HPP
The state machine engine interpreting a Traits' constexpr tables.
Definition fsm.hpp:972
Header-only C++20 hierarchical, table-driven finite state machine.
Definition dot.hpp:25
void write_dot(W &&write)
Emit a Graphviz digraph of Traits' machine through write.
Definition dot.hpp:45