Line | Branch | Exec | Source |
---|---|---|---|
1 | // This file is part of INSTINCT, the INS Toolkit for Integrated | ||
2 | // Navigation Concepts and Training by the Institute of Navigation of | ||
3 | // the University of Stuttgart, Germany. | ||
4 | // | ||
5 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
6 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
7 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
8 | |||
9 | /// @file Node.hpp | ||
10 | /// @brief Node Class | ||
11 | /// @author T. Topp (topp@ins.uni-stuttgart.de) | ||
12 | /// @date 2020-12-14 | ||
13 | |||
14 | #pragma once | ||
15 | |||
16 | #include <imgui.h> | ||
17 | #include <imgui_node_editor.h> | ||
18 | #include <imgui_stdlib.h> | ||
19 | |||
20 | #include "internal/Node/Pin.hpp" | ||
21 | #include "Navigation/Time/InsTime.hpp" | ||
22 | |||
23 | #include "util/Logger.hpp" | ||
24 | |||
25 | #include <string> | ||
26 | #include <vector> | ||
27 | #include <deque> | ||
28 | #include <thread> | ||
29 | #include <mutex> | ||
30 | #include <condition_variable> | ||
31 | #include <atomic> | ||
32 | #include <chrono> | ||
33 | #include <map> | ||
34 | |||
35 | #include <nlohmann/json.hpp> | ||
36 | using json = nlohmann::json; ///< json namespace | ||
37 | |||
38 | namespace NAV | ||
39 | { | ||
40 | class Node; | ||
41 | class NodeData; | ||
42 | class GroupBox; | ||
43 | |||
44 | namespace NodeRegistry | ||
45 | { | ||
46 | |||
47 | void RegisterNodeTypes(); // NOLINT(readability-redundant-declaration) - false warning. This is needed for the friend declaration below | ||
48 | |||
49 | } // namespace NodeRegistry | ||
50 | |||
51 | namespace FlowExecutor | ||
52 | { | ||
53 | |||
54 | /// @brief Main task of the FlowExecutor thread | ||
55 | void execute(); // NOLINT(readability-redundant-declaration) - false warning. This is needed for the friend declaration below | ||
56 | |||
57 | /// @brief Deinitialize all Nodes | ||
58 | void deinitialize(); // NOLINT(readability-redundant-declaration) - false warning. This is needed for the friend declaration below | ||
59 | |||
60 | } // namespace FlowExecutor | ||
61 | |||
62 | namespace gui | ||
63 | { | ||
64 | class NodeEditorApplication; | ||
65 | |||
66 | namespace menus | ||
67 | { | ||
68 | |||
69 | void ShowRunMenu(); | ||
70 | |||
71 | } // namespace menus | ||
72 | |||
73 | } // namespace gui | ||
74 | |||
75 | /// @brief Converts the provided node into a json object | ||
76 | /// @param[out] j Json object which gets filled with the info | ||
77 | /// @param[in] node Node to convert into json | ||
78 | void to_json(json& j, const Node& node); | ||
79 | /// @brief Converts the provided json object into a node object | ||
80 | /// @param[in] j Json object with the needed values | ||
81 | /// @param[out] node Object to fill from the json | ||
82 | void from_json(const json& j, Node& node); | ||
83 | |||
84 | /// @brief Abstract parent class for all nodes | ||
85 | class Node | ||
86 | { | ||
87 | public: | ||
88 | /// Kind information class | ||
89 | struct Kind | ||
90 | { | ||
91 | /// Possible kinds of Nodes | ||
92 | enum Value : uint8_t | ||
93 | { | ||
94 | Blueprint, ///< Node with header | ||
95 | Simple, ///< Node without header, which displays its name in the center of the content | ||
96 | GroupBox, ///< Group box which can group other nodes and drag them together | ||
97 | }; | ||
98 | |||
99 | /// @brief Default Constructor | ||
100 | Kind() = default; | ||
101 | |||
102 | /// @brief Implicit Constructor from Value type | ||
103 | /// @param[in] kind Value type to construct from | ||
104 | 6394 | constexpr Kind(Value kind) // NOLINT(hicpp-explicit-conversions, google-explicit-constructor) | |
105 | 6394 | : value(kind) | |
106 | 6394 | {} | |
107 | |||
108 | /// @brief Constructor from std::string | ||
109 | /// @param[in] string String representation of the type | ||
110 | 692 | explicit Kind(const std::string& string) | |
111 | 692 | { | |
112 |
2/2✓ Branch 1 taken 586 times.
✓ Branch 2 taken 106 times.
|
692 | if (string == "Blueprint") |
113 | { | ||
114 | 586 | value = Kind::Blueprint; | |
115 | } | ||
116 |
2/2✓ Branch 1 taken 100 times.
✓ Branch 2 taken 6 times.
|
106 | else if (string == "Simple") |
117 | { | ||
118 | 100 | value = Kind::Simple; | |
119 | } | ||
120 |
1/2✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
|
6 | else if (string == "GroupBox") |
121 | { | ||
122 | 6 | value = Kind::GroupBox; | |
123 | } | ||
124 | 692 | } | |
125 | |||
126 | /// @brief Allow switch(Node::Value(kind)) and comparisons | ||
127 | explicit operator Value() const { return value; } | ||
128 | /// @brief Prevent usage: if(node) | ||
129 | explicit operator bool() = delete; | ||
130 | /// @brief Assignment operator from Value type | ||
131 | /// @param[in] v Value type to construct from | ||
132 | /// @return The Kind type from the value type | ||
133 | 390 | Kind& operator=(Value v) | |
134 | { | ||
135 | 390 | value = v; | |
136 | 390 | return *this; | |
137 | } | ||
138 | |||
139 | friend constexpr bool operator==(const Node::Kind& lhs, const Node::Kind& rhs); | ||
140 | friend constexpr bool operator!=(const Node::Kind& lhs, const Node::Kind& rhs); | ||
141 | |||
142 | friend constexpr bool operator==(const Node::Kind& lhs, const Node::Kind::Value& rhs); | ||
143 | friend constexpr bool operator==(const Node::Kind::Value& lhs, const Node::Kind& rhs); | ||
144 | friend constexpr bool operator!=(const Node::Kind& lhs, const Node::Kind::Value& rhs); | ||
145 | friend constexpr bool operator!=(const Node::Kind::Value& lhs, const Node::Kind& rhs); | ||
146 | |||
147 | /// @brief std::string conversion operator | ||
148 | /// @return A std::string representation of the node kind | ||
149 | ✗ | explicit operator std::string() const | |
150 | { | ||
151 | ✗ | switch (value) | |
152 | { | ||
153 | ✗ | case Kind::Blueprint: | |
154 | ✗ | return "Blueprint"; | |
155 | ✗ | case Kind::Simple: | |
156 | ✗ | return "Simple"; | |
157 | ✗ | case Kind::GroupBox: | |
158 | ✗ | return "GroupBox"; | |
159 | } | ||
160 | ✗ | return ""; | |
161 | } | ||
162 | |||
163 | private: | ||
164 | /// @brief Value of the node kind | ||
165 | Value value; | ||
166 | }; | ||
167 | |||
168 | /// @brief Possible states of the node | ||
169 | enum class State : uint8_t | ||
170 | { | ||
171 | Disabled, ///< Node is disabled and won't be initialized | ||
172 | Deinitialized, ///< Node is deinitialized (red) | ||
173 | DoInitialize, ///< Node should be initialized | ||
174 | Initializing, ///< Node is currently initializing | ||
175 | Initialized, ///< Node is initialized (green) | ||
176 | DoDeinitialize, ///< Node should be deinitialized | ||
177 | Deinitializing, ///< Node is currently deinitializing | ||
178 | DoShutdown, ///< Node should shut down | ||
179 | Shutdown, ///< Node is shutting down | ||
180 | }; | ||
181 | |||
182 | /// @brief Different Modes the Node can work in | ||
183 | enum class Mode : uint8_t | ||
184 | { | ||
185 | REAL_TIME, ///< Node running in real-time mode | ||
186 | POST_PROCESSING, ///< Node running in post-processing mode | ||
187 | }; | ||
188 | |||
189 | /// @brief Constructor | ||
190 | /// @param[in] name Name of the node | ||
191 | explicit Node(std::string name); | ||
192 | /// @brief Destructor | ||
193 | virtual ~Node(); | ||
194 | /// @brief Copy constructor | ||
195 | Node(const Node&) = delete; | ||
196 | /// @brief Move constructor | ||
197 | Node(Node&&) = delete; | ||
198 | /// @brief Copy assignment operator | ||
199 | Node& operator=(const Node&) = delete; | ||
200 | /// @brief Move assignment operator | ||
201 | Node& operator=(Node&&) = delete; | ||
202 | |||
203 | /* -------------------------------------------------------------------------------------------------------- */ | ||
204 | /* Interface */ | ||
205 | /* -------------------------------------------------------------------------------------------------------- */ | ||
206 | |||
207 | /// @brief String representation of the Class Type | ||
208 | [[nodiscard]] virtual std::string type() const = 0; | ||
209 | |||
210 | /// @brief ImGui config window which is shown on double click | ||
211 | /// @attention Don't forget to set hasConfig to true | ||
212 | virtual void guiConfig(); | ||
213 | |||
214 | /// @brief Saves the node into a json object | ||
215 | [[nodiscard]] virtual json save() const; | ||
216 | |||
217 | /// @brief Restores the node from a json object | ||
218 | /// @param[in] j Json object with the node state | ||
219 | virtual void restore(const json& j); | ||
220 | |||
221 | /// @brief Restores link related properties of the node from a json object | ||
222 | /// @param[in] j Json object with the node state | ||
223 | virtual void restoreAtferLink(const json& j); | ||
224 | |||
225 | /// @brief Initialize the Node | ||
226 | virtual bool initialize(); | ||
227 | |||
228 | /// @brief Deinitialize the Node | ||
229 | virtual void deinitialize(); | ||
230 | |||
231 | /// @brief Resets the node. It is guaranteed that the node is initialized when this is called. | ||
232 | virtual bool resetNode(); | ||
233 | |||
234 | /// @brief Called when a new link is to be established | ||
235 | /// @param[in] startPin Pin where the link starts | ||
236 | /// @param[in] endPin Pin where the link ends | ||
237 | /// @return True if link is allowed, false if link is rejected | ||
238 | virtual bool onCreateLink(OutputPin& startPin, InputPin& endPin); | ||
239 | |||
240 | /// @brief Called when a link is to be deleted | ||
241 | /// @param[in] startPin Pin where the link starts | ||
242 | /// @param[in] endPin Pin where the link ends | ||
243 | virtual void onDeleteLink(OutputPin& startPin, InputPin& endPin); | ||
244 | |||
245 | /// @brief Called when a new link was established | ||
246 | /// @param[in] startPin Pin where the link starts | ||
247 | /// @param[in] endPin Pin where the link ends | ||
248 | virtual void afterCreateLink(OutputPin& startPin, InputPin& endPin); | ||
249 | |||
250 | /// @brief Called when a link was deleted | ||
251 | /// @param[in] startPin Pin where the link starts | ||
252 | /// @param[in] endPin Pin where the link ends | ||
253 | virtual void afterDeleteLink(OutputPin& startPin, InputPin& endPin); | ||
254 | |||
255 | /// @brief Function called by the flow executer after finishing to flush out remaining data | ||
256 | virtual void flush(); | ||
257 | |||
258 | /* -------------------------------------------------------------------------------------------------------- */ | ||
259 | /* Member functions */ | ||
260 | /* -------------------------------------------------------------------------------------------------------- */ | ||
261 | |||
262 | /// @brief Notifies connected nodes about the change | ||
263 | /// @param[in] pinIdx Output Port index where to set the value | ||
264 | /// @param[in] insTime Time the value was generated | ||
265 | /// @param[in] guard Lock guard of the output data | ||
266 | void notifyOutputValueChanged(size_t pinIdx, const InsTime& insTime, const std::scoped_lock<std::mutex>& guard); | ||
267 | |||
268 | /// @brief Blocks the thread till the output values was read by all connected nodes | ||
269 | /// @param[in] pinIdx Output Pin index where to request the lock | ||
270 | [[nodiscard]] std::scoped_lock<std::mutex> requestOutputValueLock(size_t pinIdx); | ||
271 | |||
272 | /// @brief Get Input Value connected on the pin. Only const data types. | ||
273 | /// @tparam T Type of the connected object | ||
274 | /// @param[in] portIndex Input port where to retrieve the data from | ||
275 | /// @return Pointer to the object | ||
276 | template<typename T> | ||
277 | 701 | [[nodiscard]] std::optional<InputPin::IncomingLink::ValueWrapper<T>> getInputValue(size_t portIndex) const | |
278 | { | ||
279 | 701 | return inputPins.at(portIndex).link.getValue<T>(); | |
280 | } | ||
281 | |||
282 | /// @brief Unblocks the connected node. Has to be called when the input value should be released and getInputValue was not called. | ||
283 | /// @param[in] portIndex Input port where the data should be released | ||
284 | void releaseInputValue(size_t portIndex); | ||
285 | |||
286 | /// @brief Calls all registered callbacks on the specified output port | ||
287 | /// @param[in] portIndex Output port where to call the callbacks | ||
288 | /// @param[in] data The data to pass to the callback targets | ||
289 | void invokeCallbacks(size_t portIndex, const std::shared_ptr<const NodeData>& data); | ||
290 | |||
291 | /// @brief Returns the pin with the given id | ||
292 | /// @param[in] pinId Id of the Pin | ||
293 | /// @return The input pin | ||
294 | [[nodiscard]] InputPin& inputPinFromId(ax::NodeEditor::PinId pinId); | ||
295 | |||
296 | /// @brief Returns the pin with the given id | ||
297 | /// @param[in] pinId Id of the Pin | ||
298 | /// @return The output pin | ||
299 | [[nodiscard]] OutputPin& outputPinFromId(ax::NodeEditor::PinId pinId); | ||
300 | |||
301 | /// @brief Returns the index of the pin | ||
302 | /// @param[in] pinId Id of the Pin | ||
303 | /// @return The index of the pin | ||
304 | [[nodiscard]] size_t inputPinIndexFromId(ax::NodeEditor::PinId pinId) const; | ||
305 | |||
306 | /// @brief Returns the index of the pin | ||
307 | /// @param[in] pinId Id of the Pin | ||
308 | /// @return The index of the pin | ||
309 | [[nodiscard]] size_t outputPinIndexFromId(ax::NodeEditor::PinId pinId) const; | ||
310 | |||
311 | /// @brief Node name and id | ||
312 | [[nodiscard]] std::string nameId() const; | ||
313 | |||
314 | /// @brief Get the size of the node | ||
315 | [[nodiscard]] const ImVec2& getSize() const; | ||
316 | |||
317 | // ------------------------------------------ State handling --------------------------------------------- | ||
318 | |||
319 | /// @brief Converts the state into a printable text | ||
320 | /// @param[in] state State to convert | ||
321 | /// @return String representation of the state | ||
322 | static std::string toString(State state); | ||
323 | |||
324 | /// @brief Get the current state of the node | ||
325 | [[nodiscard]] State getState() const; | ||
326 | |||
327 | /// @brief Get the current mode of the node | ||
328 | [[nodiscard]] Mode getMode() const; | ||
329 | |||
330 | /// @brief Asks the node worker to initialize the node | ||
331 | /// @param[in] wait Wait for the worker to complete the request | ||
332 | /// @return True if not waiting and the worker accepted the request otherwise if waiting only true if the node initialized correctly | ||
333 | bool doInitialize(bool wait = false); | ||
334 | |||
335 | /// @brief Asks the node worker to reinitialize the node | ||
336 | /// @param[in] wait Wait for the worker to complete the request | ||
337 | /// @return True if not waiting and the worker accepted the request otherwise if waiting only true if the node initialized correctly | ||
338 | bool doReinitialize(bool wait = false); | ||
339 | |||
340 | /// @brief Asks the node worker to deinitialize the node | ||
341 | /// @param[in] wait Wait for the worker to complete the request | ||
342 | /// @return True if the worker accepted the request | ||
343 | bool doDeinitialize(bool wait = false); | ||
344 | |||
345 | /// @brief Asks the node worker to disable the node | ||
346 | /// @param[in] wait Wait for the worker to complete the request | ||
347 | /// @return True if the worker accepted the request | ||
348 | bool doDisable(bool wait = false); | ||
349 | |||
350 | /// @brief Enable the node | ||
351 | /// @return True if enabling was successful | ||
352 | bool doEnable(); | ||
353 | |||
354 | /// Wakes the worker thread | ||
355 | void wakeWorker(); | ||
356 | |||
357 | /// @brief Checks if the node is disabled | ||
358 | [[nodiscard]] bool isDisabled() const; | ||
359 | |||
360 | /// @brief Checks if the node is initialized | ||
361 | [[nodiscard]] bool isInitialized() const; | ||
362 | |||
363 | /// @brief Checks if the node is changing its state currently | ||
364 | [[nodiscard]] bool isTransient() const; | ||
365 | |||
366 | /// @brief Checks if the node is only working in real time (sensors, network interfaces, ...) | ||
367 | [[nodiscard]] bool isOnlyRealtime() const; | ||
368 | |||
369 | /* -------------------------------------------------------------------------------------------------------- */ | ||
370 | /* Member variables */ | ||
371 | /* -------------------------------------------------------------------------------------------------------- */ | ||
372 | |||
373 | /// Unique Id of the Node | ||
374 | ax::NodeEditor::NodeId id = 0; | ||
375 | /// Kind of the Node | ||
376 | Kind kind = Kind::Blueprint; | ||
377 | /// Name of the Node | ||
378 | std::string name; | ||
379 | /// List of input pins | ||
380 | std::vector<InputPin> inputPins; | ||
381 | /// List of output pins | ||
382 | std::vector<OutputPin> outputPins; | ||
383 | |||
384 | /// Enables the callbacks | ||
385 | bool callbacksEnabled = false; | ||
386 | |||
387 | /// Map with callback events (sorted by time) | ||
388 | std::multimap<InsTime, std::pair<OutputPin*, size_t>> pollEvents; | ||
389 | |||
390 | protected: | ||
391 | /// The Default Window size for new config windows. | ||
392 | /// Only set the variable if the object/window has no persistently saved data (no entry in .ini file) | ||
393 | ImVec2 _guiConfigDefaultWindowSize{ 500.0F, 400.0F }; | ||
394 | |||
395 | /// Flag if the config window should be shown | ||
396 | bool _hasConfig = false; | ||
397 | |||
398 | /// Lock the config when executing post-processing | ||
399 | bool _lockConfigDuringRun = true; | ||
400 | |||
401 | /// Whether the node can run in post-processing or only real-time | ||
402 | bool _onlyRealTime = false; | ||
403 | |||
404 | private: | ||
405 | State _state = State::Deinitialized; ///< Current state of the node | ||
406 | mutable std::mutex _stateMutex; ///< Mutex to interact with the worker state variable | ||
407 | |||
408 | /// Mode the node is currently running in | ||
409 | std::atomic<Mode> _mode = Mode::REAL_TIME; | ||
410 | |||
411 | /// Flag if the node should be reinitialize after deinitializing | ||
412 | bool _reinitialize = false; | ||
413 | |||
414 | /// Flag if the node should be disabled after deinitializing | ||
415 | bool _disable = false; | ||
416 | |||
417 | /// Flag if the config window is shown | ||
418 | bool _showConfig = false; | ||
419 | |||
420 | /// Mutex to show the config window (prevents initialization to modify values within the config window) | ||
421 | std::mutex _configWindowMutex; | ||
422 | /// Flag if the config window should be forced collapsed | ||
423 | bool _configWindowForceCollapse = false; | ||
424 | /// Flag if the config window is collapsed | ||
425 | bool _configWindowIsCollapsed = false; | ||
426 | |||
427 | /// Flag if the config window should be focused | ||
428 | bool _configWindowFocus = false; | ||
429 | |||
430 | /// Size of the node in pixels | ||
431 | ImVec2 _size{ 0, 0 }; | ||
432 | |||
433 | /// Flag which prevents the worker to be autostarted if false | ||
434 | static inline bool _autostartWorker = true; | ||
435 | |||
436 | 6394 | std::chrono::duration<int64_t> _workerTimeout = std::chrono::minutes(1); ///< Periodic timeout of the worker to check if new data available | |
437 | std::thread _worker; ///< Worker handling initialization and processing of data | ||
438 | std::mutex _workerMutex; ///< Mutex to interact with the worker condition variable | ||
439 | std::condition_variable _workerConditionVariable; ///< Condition variable to signal the worker thread to do something | ||
440 | bool _workerWakeup = false; ///< Variable to prevent the worker from sleeping | ||
441 | |||
442 | /// @brief Worker thread | ||
443 | /// @param[in, out] node The node where the thread belongs to | ||
444 | static void workerThread(Node* node); | ||
445 | |||
446 | /// Handler which gets triggered if the worker runs into a periodic timeout | ||
447 | virtual void workerTimeoutHandler(); | ||
448 | |||
449 | /// @brief Called by the worker to initialize the node | ||
450 | /// @return True if the initialization was successful | ||
451 | bool workerInitializeNode(); | ||
452 | |||
453 | /// @brief Called by the worker to deinitialize the node | ||
454 | /// @return True if the deinitialization was successful | ||
455 | bool workerDeinitializeNode(); | ||
456 | |||
457 | friend class gui::NodeEditorApplication; | ||
458 | friend class NAV::GroupBox; | ||
459 | |||
460 | /// @brief Main task of the FlowExecutor thread | ||
461 | friend void NAV::FlowExecutor::execute(); | ||
462 | /// @brief Deinitialize all Nodes | ||
463 | friend void NAV::FlowExecutor::deinitialize(); | ||
464 | /// @brief Register all available Node types for the program | ||
465 | friend void NAV::NodeRegistry::RegisterNodeTypes(); | ||
466 | |||
467 | /// @brief Converts the provided node into a json object | ||
468 | /// @param[out] j Json object which gets filled with the info | ||
469 | /// @param[in] node Node to convert into json | ||
470 | friend void NAV::to_json(json& j, const Node& node); | ||
471 | /// @brief Converts the provided json object into a node object | ||
472 | /// @param[in] j Json object with the needed values | ||
473 | /// @param[out] node Object to fill from the json | ||
474 | friend void NAV::from_json(const json& j, Node& node); | ||
475 | |||
476 | /// @brief Show the run menu dropdown | ||
477 | friend void gui::menus::ShowRunMenu(); | ||
478 | }; | ||
479 | |||
480 | /// @brief Equal compares Node::Kind values | ||
481 | /// @param[in] lhs Left-hand side of the operator | ||
482 | /// @param[in] rhs Right-hand side of the operator | ||
483 | /// @return Whether the comparison was successful | ||
484 | constexpr bool operator==(const Node::Kind& lhs, const Node::Kind& rhs) { return lhs.value == rhs.value; } | ||
485 | /// @brief Inequal compares Node::Kind values | ||
486 | /// @param[in] lhs Left-hand side of the operator | ||
487 | /// @param[in] rhs Right-hand side of the operator | ||
488 | /// @return Whether the comparison was successful | ||
489 | constexpr bool operator!=(const Node::Kind& lhs, const Node::Kind& rhs) { return !(lhs == rhs); } | ||
490 | |||
491 | /// @brief Equal compares Node::Kind values | ||
492 | /// @param[in] lhs Left-hand side of the operator | ||
493 | /// @param[in] rhs Right-hand side of the operator | ||
494 | /// @return Whether the comparison was successful | ||
495 | 3489 | constexpr bool operator==(const Node::Kind& lhs, const Node::Kind::Value& rhs) { return lhs.value == rhs; } | |
496 | /// @brief Equal compares Node::Kind values | ||
497 | /// @param[in] lhs Left-hand side of the operator | ||
498 | /// @param[in] rhs Right-hand side of the operator | ||
499 | /// @return Whether the comparison was successful | ||
500 | constexpr bool operator==(const Node::Kind::Value& lhs, const Node::Kind& rhs) { return lhs == rhs.value; } | ||
501 | /// @brief Inequal compares Node::Kind values | ||
502 | /// @param[in] lhs Left-hand side of the operator | ||
503 | /// @param[in] rhs Right-hand side of the operator | ||
504 | /// @return Whether the comparison was successful | ||
505 | 2067 | constexpr bool operator!=(const Node::Kind& lhs, const Node::Kind::Value& rhs) { return !(lhs == rhs); } | |
506 | /// @brief Inequal compares Node::Kind values | ||
507 | /// @param[in] lhs Left-hand side of the operator | ||
508 | /// @param[in] rhs Right-hand side of the operator | ||
509 | /// @return Whether the comparison was successful | ||
510 | constexpr bool operator!=(const Node::Kind::Value& lhs, const Node::Kind& rhs) { return !(lhs == rhs); } | ||
511 | |||
512 | } // namespace NAV | ||
513 |