Invocation: build a DSL to verify connectivity (see #1377)

This was a lot of intricate technical work,
and is now verified in-depth, covering all possible cases.

__We can now__
 * build Nodes
 * verify in detail correct connectivity
 * read Node-IDs and processing specifications
 * maintain a symbolic spec for the arguments of a Port

(and beyond that, we can also **invoke nodes**, which remains to be formally verified)
This commit is contained in:
Fischlurch 2025-02-05 00:25:02 +01:00
parent d5a47bf3e4
commit 72d7986b5e
8 changed files with 564 additions and 135 deletions

View file

@ -227,6 +227,10 @@ namespace lib {
friend auto begin (Several const& svl) { return svl.begin();}
friend auto end (Several const& svl) { return svl.end(); }
using value_type = typename meta::RefTraits<I>::Value;
using reference = typename meta::RefTraits<I>::Reference;
using const_reference = value_type const&;
protected:
size_t

View file

@ -13,9 +13,11 @@
/** @file proc-node.cpp
** Translation unit to hold the actual implementation of node processing operations.
**
** @todo WIP-WIP-WIP 6/2024 not clear yet what goes here and what goes into turnout-system.cpp
** Translation unit for implementation details regarding node-IDs and verification.
** @remark \ref ProcNode itself is a shell to provide a node-ID and a high-level API
** for Render Node invocation. The actual implementation of processing functionality
** is located within the [Turnout](\ref turnout.hpp) and the individual »weaving patterns«
** embedded therein.
*/
@ -247,7 +249,7 @@ namespace engine {
ProcID::genProcSpec() const
{
std::ostringstream buffer;
buffer << nodeName_
buffer << genNodeSymbol()
<< genQualifier()
<< argLists_;
return buffer.str();
@ -501,17 +503,135 @@ namespace engine {
{
auto& leadPorts = srcPorts();
return input < leadPorts.size()
and isSameObject (leadPorts[input].get(), tarPort);
and leadPorts[input] == tarPort;
}
bool
PortDiagnostic::verify_connected (Port& tarPort)
{
for (Port& port : srcPorts())
if (isSameObject (port, tarPort))
if (port == tarPort)
return true;
return false;
}
/**
* @remark _ConCheck provides a fluent DSL notation to verify node connectivity.
* This is achieved by first collecting some counterparts and index specifications
* for the kind of connection to validate. Each qualifier just sets a parameter and
* returns the _ConCheck object by move. The final result is retrieved by this bool
* conversion — which in fact has to implement a collection of different evaluations.
* The proper kind of evaluation will be picked based on the actual arguments given;
* the more are present, the more constricted the evaluation becomes. The selection
* of the evaluation to take is thus ordered in reverse order, starting with the
* most constricted cases. Three different kinds of link validations are provided
* - compare two fully qualified Port objects for identity (same object)
* - check if a given Port object is present in a collection of Ports
* - exhaustive search for a match in the cross product of two Port collections.
* The last case is what allows to perform a high-level connectivity test between
* two nodes, which are considered as connected if any link is found.
* @see NodeMeta_test::verify_ID_connectivity()
*/
_ConCheck::operator bool()
{
auto validPort = [this](uint idx) { return idx < anchor.ports().size(); };
auto validLead = [this](uint idx) { return idx < anchor.leads().size(); };
auto validSrc = [this](uint pNo
,uint sNo) { return sNo < anchor.watchPort(pNo).srcPorts().size(); };
auto validSrcP = [this](ProcNode& lead
,uint idx) { return idx < watch(lead).ports().size(); };
auto find_link = [](auto& seq1, auto& seq2)
{
return explore(seq1)
.transform([&](auto& elm){ return contains (seq2, *elm); })
.has_any();
};
// Determine case to handle,
// starting with the most constricted...
if (portNo and srcNo and srcNode and srcPNo)
return validPort(*portNo) // is_linked(node).port(portNo).asSrc(srcNo).to(srcNode).atPort(srcPNo)
and validSrc (*portNo, *srcNo)
and validSrcP(*srcNode,*srcPNo)
and anchor.watchPort(*portNo).srcPorts()[*srcNo]
== watch(*srcNode).ports()[*srcPNo];
else
if (portNo and srcNo and leadNo and srcPNo)
return validPort(*portNo) // is_linked(node).port(portNo).asSrc(srcNo).toLead(leadNo).atPort(srcPNo)
and validSrc (*portNo,*srcNo)
and validLead(*leadNo)
and validSrcP(anchor.leads()[*leadNo], *srcPNo)
and anchor.watchPort(*portNo).srcPorts()[*srcNo]
== anchor.watchLead(*leadNo).ports()[*srcPNo];
else
if (portNo and srcNo and srcPort)
return validPort(*portNo) // is_linked(node).port(portNo).asSrc(srcNo).to(srcPort)
and validSrc (*portNo,*srcNo)
and anchor.watchPort(*portNo).srcPorts()[*srcNo]
== *srcPort;
else
if (portNo and srcNo and srcNode)
return validPort(*portNo) // is_linked(node).port(portNo).asSrc(srcNo).to(srcNode)
and validSrc (*portNo,*srcNo)
and contains (watch(*srcNode).ports()
,anchor.watchPort(*portNo).srcPorts()[*srcNo]);
else
if (portNo and srcNo and leadNo)
return validPort(*portNo) // is_linked(node).port(portNo).asSrc(srcNo).toLead(leadNo)
and validSrc (*portNo,*srcNo)
and validLead(*leadNo)
and contains (anchor.watchLead(*leadNo).ports()
,anchor.watchPort(*portNo).srcPorts()[*srcNo]);
else
if (portNo and srcNo)
return validPort(*portNo) // is_linked(node).port(portNo).asSrc(srcNo)
and validSrc (*portNo,*srcNo);
else
if (portNo and srcNode and srcPNo)
return validPort(*portNo) // is_linked(node).port(portNo).to(srcNode).atPort(srcPNo)
and validSrcP(*srcNode,*srcPNo)
and contains (anchor.watchPort(*portNo).srcPorts()
,watch(*srcNode).ports()[*srcPNo]);
else
if (portNo and leadNo and srcPNo)
return validPort(*portNo) // is_linked(node).port(portNo).toLead(leadNo).atPort(srcPNo)
and validLead(*leadNo)
and validSrcP(anchor.leads()[*leadNo], *srcPNo)
and contains (anchor.watchPort(*portNo).srcPorts()
,anchor.watchLead(*leadNo).ports()[*srcPNo]);
else
if (portNo and srcPort)
return validPort(*portNo) // is_linked(node).port(portNo).to(srcPort)
and contains (anchor.watchPort(*portNo).srcPorts()
,*srcPort);
else
if (portNo and srcNode)
return validPort(*portNo) // is_linked(node).port(portNo).to(srcNode)
and find_link(watch(*srcNode).ports()
,anchor.watchPort(*portNo).srcPorts());
else
if (portNo and leadNo)
return validPort(*portNo) // is_linked(node).port(portNo).toLead(leadNo)
and validLead(*leadNo)
and find_link(anchor.watchLead(*leadNo).ports()
,anchor.watchPort(*portNo).srcPorts());
else
if (portNo)
return validPort(*portNo) // is_linked(node).port(portNo)
and not anchor.watchPort(*portNo).isSrc();
else
if (srcNode and leadNo)
return validLead(*leadNo) // is_linked(node).to(srcNode).asLead(leadNo)
and anchor.leads()[*leadNo]
== *srcNode;
else
if (srcNode)
return contains (anchor.leads() // is_linked(node).to(srcNode)
,*srcNode);
return false;
}
}} // namespace steam::engine

View file

@ -84,7 +84,7 @@
** @remark A future extension to this scheme is conceivable, where common processing pipelines
** are pre-compiled in entirety, possibly combined with hardware acceleration.
**
** @todo WIP-WIP 12/2024 Node-Invocation is reworked from ground up for the »Playback Vertical Slice«
** @todo WIP 2/2025 Node-Invocation is reworked from ground up for the »Playback Vertical Slice«
**
** @see turnout.hpp
** @see turnout-system.hpp
@ -99,11 +99,8 @@
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "lib/hash-value.h"
//#include "steam/asset/proc.hpp"
//#include "steam/mobject/parameter.hpp"
#include "steam/engine/buffhandle.hpp"
#include "steam/engine/turnout-system.hpp"
#include "lib/ref-array.hpp" /////////////////////OOO phase out
#include "lib/format-string.hpp"
#include "lib/several.hpp"
@ -118,6 +115,8 @@ namespace engine {
using std::move;
using std::string;
using std::nullopt;
using std::optional;
using lib::HashVal;
using util::_Fmt;
@ -145,6 +144,10 @@ namespace engine {
virtual BuffHandle weave (TurnoutSystem&, OptionalBuff =std::nullopt) =0;
ProcID& procID;
/// Port has reference semantics: all instances are distinct
friend bool operator== (Port const& pl, Port const& pr){ return & pl == & pr;}
friend bool operator!= (Port const& pl, Port const& pr){ return not (pl == pr); }
};
using PortRef = std::reference_wrapper<Port>;
@ -232,6 +235,10 @@ namespace engine {
return wiring_.ports[portIdx];
}
/// ProcNode has reference semantics: all instances are distinct
friend bool operator== (ProcNode const& nl, ProcNode const& nr){ return & nl == & nr;}
friend bool operator!= (ProcNode const& nl, ProcNode const& nr){ return not (nl == nr); }
/*************************************************************//**
* Engine Core operation: render and pull output from this node.
@ -333,5 +340,47 @@ namespace engine {
}
/** Helper for connectivity-checks in tests */
class _ConCheck
: util::MoveOnly
{
ProcNodeDiagnostic anchor;
ProcNode* srcNode{nullptr};
Port* srcPort{nullptr};
optional<uint> leadNo{nullopt};
optional<uint> portNo{nullopt};
optional<uint> srcNo {nullopt};
optional<uint> srcPNo{nullopt};
public:
_ConCheck (ProcNode& n)
: anchor{watch(n)}
{ }
/** implement decision logic based on context given. */
operator bool();
_ConCheck to (ProcNode& n) { srcNode = &n; return move(*this); }
_ConCheck to (Port& p) { srcPort = &p; return move(*this); }
_ConCheck asLead (uint idx){ leadNo = idx; return move(*this); }
_ConCheck toLead (uint idx){ leadNo = idx; return move(*this); }
_ConCheck asSrc (uint idx){ srcNo = idx; return move(*this); }
_ConCheck port (uint idx){ portNo = idx; return move(*this); }
_ConCheck atPort (uint idx){ srcPNo = idx; return move(*this); }
};
/**
* start a DSL expression to verify node connectivity.
* @see NodeMeta_test::verify_ID_connectivity()
*/
inline _ConCheck
is_linked (ProcNode& n)
{
return _ConCheck{n};
}
}} // namespace steam::engine
#endif /*STEAM_ENGINE_PROC_NODE_H*/

View file

@ -22,7 +22,7 @@ PLANNED "Proc Node connectivity" NodeLink_test <<END
END
PLANNED "Proc Node metadata key" NodeMeta_test <<END
Test "Proc Node metadata key" NodeMeta_test <<END
END

View file

@ -208,6 +208,9 @@ namespace test {
.completePort()
.build()};
CHECK (is_linked(n3).to(n1));
CHECK (is_linked(n3).to(n2));
uint res = invokeRenderNode(n3);
CHECK (res == peek+1 + LIFE_AND_UNIVERSE_4EVER+1 );
CHECK (peek != -1);

View file

@ -90,7 +90,7 @@ namespace test {
CHECK (watch(n1).ports().size() == 1);
// can generate a symbolic spec to describe the Port's processing functionality...
CHECK (watch(n1).getPortSpec(0) == "Test:dummy.op(int)"_expect);
CHECK (watch(n1).getPortSpec(0) == "dummy.op(int)"_expect);
CHECK (watch(n1).getPortSpec(1) == "↯"_expect);
// such a symbolic spec is actually generated by a deduplicated metadata descriptor
@ -111,15 +111,15 @@ namespace test {
// re-generate the descriptor for the source node (n1)
auto& metaN1 = ProcID::describe("Test:dummy","op(int)");
CHECK (metaN1.genProcSpec() == "Test:dummy.op(int)"_expect);
CHECK (metaN1.genProcName() == "Test:dummy.op"_expect);
CHECK (metaN1.genProcSpec() == "dummy.op(int)"_expect);
CHECK (metaN1.genProcName() == "dummy.op"_expect);
CHECK (metaN1.genNodeName() == "Test:dummy"_expect);
CHECK (metaN1.genNodeSpec(con.leads) == "Test:dummy-◎"_expect);
}
/** @test TODO Build more elaborate Render Nodes linked into a connectivity network
* @todo WIP 1/25 🔁 define ⟶ implement
/** @test Build more elaborate Render Nodes linked into a connectivity network
* @todo WIP 1/25 🔁 define ⟶ ✔ implement
*/
void
build_connected_nodes()
@ -197,6 +197,13 @@ SHOW_EXPR(watch(n1).getPortSpec(0))
SHOW_EXPR(watch(n1).getPortSpec(1))
SHOW_EXPR(watch(n1.getPort(0)).getProcSpec())
SHOW_EXPR(watch(n1.getPort(0)).isSrc())
CHECK ( is_linked(n2).to(n1));
CHECK (not is_linked(n1b).to(n1));
CHECK (not is_linked(n3).to(n1));
CHECK ( is_linked(n3).to(n1b));
CHECK ( is_linked(n3).to(n2));
}

View file

@ -21,26 +21,22 @@
#include "steam/engine/node-builder.hpp"
#include "lib/format-util.hpp"
#include "lib/test/test-helper.hpp"
//#include "steam/engine/test-rand-ontology.hpp" ///////////TODO
#include "lib/test/diagnostic-output.hpp"/////////////////TODO
//#include "lib/util.hpp"
#include <cmath>
//using std::string;
namespace steam {
namespace engine{
namespace test {
using std::abs;
// using util::join;
/***************************************************************//**
* @test Render node metadata and hash identity keys.
* @todo 2/2025 hash computation is not yet specified...
*/
class NodeMeta_test : public Test
{
@ -77,8 +73,8 @@ namespace test {
CHECK (p2.genProcName() == "N2.+"_expect ); // domain omitted, qualifier joined with '.'
CHECK (p2.genQualifier() == ".+"_expect ); // qualifier includes leading '.'
CHECK (p3.genProcName() == "N3"_expect );
CHECK (p2.genProcSpec() == "U:N2.+(a1,a2)"_expect );
CHECK (p3.genProcSpec() == "O:N3(in/3)(o1,o2/2)"_expect );
CHECK (p2.genProcSpec() == "N2.+(a1,a2)"_expect );
CHECK (p3.genProcSpec() == "N3(in/3)(o1,o2/2)"_expect );
ProcID::ArgModel arg1 = p1.genArgModel();
ProcID::ArgModel arg2 = p2.genArgModel();
@ -109,13 +105,31 @@ namespace test {
}
/** @test TODO aspects of node definition relevant for the ProcID
* @todo WIP 2/25 🔁 define ⟶ 🔁 implement
/** @test validate the interplay of node connectivity
* with reported properties at the ProcID and
* demonstrate tools to check connectivity.
* - Build a 3-node network with dummy operations,
* which however are built to mimic the very common
* situation where two sources are mixed
* - at exit side, three different »flavours« can be
* produced, which implies that there are three Ports.
* - the source at the »A-side« provided only two flavours,
* and thus an explicit wiring has to be made for the
* A-side connection of the third chain
* - In real usage, the node specification strings will be
* provided from the Media-Lib adapter plug-in. Here it is
* hard wired, and defined in a way to reflect structure.
* - various ways to drill-down into the structure are explored
* by verifying the ProcID specification visible at each point.
* - then the tools for verifying connectivity are demonstrated
* and covered with relevant positive and negative combinations.
* @todo 2/25 🔁 define ⟶ ✔ implement
*/
void
verify_ID_connectivity()
{
// This operation emulates a data source
// These operations emulate data sources
auto src_opA = [](int param, int* res) { *res = param; };
auto src_opB = [](ulong param, ulong* res){ *res = param; };
@ -214,7 +228,8 @@ namespace test {
CHECK (watch(nM).watchPort(2).watchLead(1).getProcName() == "srcB.c"_expect );
CHECK (watch(nM).watchPort(2).watchLead(1).getProcSpec() == "srcB.c(ulong)"_expect);
CHECK (watch(nM).watchPort(2).watchLead(1).isSrc() == true ); // the lead port itself is a source
CHECK (watch(nM).watchPort(2).watchLead(1).srcPorts().size() == 0 );
CHECK (watch(nM).watchPort(2).watchLead(1).srcPorts().size() == 0 ); // ...and thus has an empty source-port-collection
// Helper predicate to verify connectedness to a specific Port given by reference
CHECK (watch(nM).watchPort(2).verify_connected( watch(nA).ports()[0]) == false );
@ -227,8 +242,169 @@ namespace test {
CHECK (watch(nM).watchPort(2).verify_connected(0, watch(nB).ports()[2]) == false );
CHECK (watch(nM).watchPort(2).verify_connected(1, watch(nA).ports()[1]) == false ); // Node-nM.port#2 doesn't connect via source#1 to Node-nA.port#1
///////////////////////////////////////////////////////TODO WIP
UNIMPLEMENTED ("verify connectivity");
//__________________________________
// Inspect Node and Port connectivity
// High-level case: connections between nodes
CHECK (is_linked(nM).to(nA) == true );
CHECK (is_linked(nM).to(nB) == true );
CHECK (is_linked(nA).to(nB) == false);
// additionally qualify the index position
// of the source node in the sequence of »Lead nodes«
CHECK (is_linked(nM).to(nA).asLead(0) == true ); // Node-nA is Lead-#0
CHECK (is_linked(nM).to(nA).asLead(1) == false);
CHECK (is_linked(nM).to(nB).asLead(0) == false);
CHECK (is_linked(nM).to(nB).asLead(1) == true ); // Node-nB is Lead-#1
// Check if a specific Port is connected to a source node
CHECK (is_linked(nM).port(0).to(nA) == true );
CHECK (is_linked(nM).port(0).to(nB) == true );
CHECK (is_linked(nM).port(0).to(nM) == false); // never connected to itself
// Similar, but now pick the source node from the »Leads«
CHECK (is_linked(nM).port(0).toLead(0) == true );
CHECK (is_linked(nM).port(0).toLead(1) == true );
CHECK (is_linked(nA).port(0).toLead(0) == false); // nA is a source node and thus has no further source-connections
// Verify detailed port-to-port connectivity
CHECK (is_linked(nM).port(0).to(watch(nA).ports()[0]) == true ); // Node-nM connected within Port-0 to Port-0 of Node-nA
CHECK (is_linked(nM).port(0).to(watch(nA).ports()[1]) == false); // ......but not connected to Port-1 of Node-nA
CHECK (is_linked(nM).port(0).to(watch(nB).ports()[0]) == true );
CHECK (is_linked(nM).port(0).to(watch(nB).ports()[1]) == false);
CHECK (is_linked(nM).port(0).to(watch(nB).ports()[2]) == false);
CHECK (is_linked(nM).port(2).to(watch(nA).ports()[0]) == false);
CHECK (is_linked(nM).port(2).to(watch(nA).ports()[1]) == true ); // this is the connection routed from port-2 to Node-nA, Port-1
CHECK (is_linked(nM).port(2).to(watch(nB).ports()[0]) == false);
CHECK (is_linked(nM).port(2).to(watch(nB).ports()[1]) == false);
CHECK (is_linked(nM).port(2).to(watch(nB).ports()[2]) == true );
CHECK (is_linked(nM).port(2).to(watch(nM).ports()[2]) == false); // a nonsensical check, nodes are never connected to themselves
CHECK (is_linked(nM).port(0).to(nA).atPort(0) == true );
CHECK (is_linked(nM).port(0).to(nA).atPort(1) == false);
CHECK (is_linked(nM).port(0).to(nB).atPort(0) == true );
CHECK (is_linked(nM).port(0).to(nB).atPort(1) == false);
CHECK (is_linked(nM).port(0).to(nB).atPort(2) == false);
CHECK (is_linked(nM).port(2).to(nA).atPort(0) == false);
CHECK (is_linked(nM).port(2).to(nA).atPort(1) == true );
CHECK (is_linked(nM).port(2).to(nB).atPort(0) == false);
CHECK (is_linked(nM).port(2).to(nB).atPort(1) == false);
CHECK (is_linked(nM).port(2).to(nB).atPort(2) == true );
CHECK (is_linked(nM).port(2).to(nM).atPort(2) == false);
CHECK (is_linked(nM).port(0).toLead(0).atPort(0) == true );
CHECK (is_linked(nM).port(0).toLead(0).atPort(1) == false);
CHECK (is_linked(nM).port(0).toLead(1).atPort(0) == true );
CHECK (is_linked(nM).port(0).toLead(1).atPort(1) == false);
CHECK (is_linked(nM).port(0).toLead(1).atPort(2) == false);
CHECK (is_linked(nM).port(2).toLead(0).atPort(0) == false);
CHECK (is_linked(nM).port(2).toLead(0).atPort(1) == true );
CHECK (is_linked(nM).port(2).toLead(1).atPort(0) == false);
CHECK (is_linked(nM).port(2).toLead(1).atPort(1) == false);
CHECK (is_linked(nM).port(2).toLead(1).atPort(2) == true );
// additionally also qualify the «source slot«
// at which the connection is used as input for the processing-function
CHECK (is_linked(nM).port(0).asSrc(0).to(nA) == true ); // Node-nM, Port-0 uses as source-slot-0 a connection to Node-nA
CHECK (is_linked(nM).port(0).asSrc(1).to(nA) == false );
CHECK (is_linked(nM).port(0).asSrc(0).to(nB) == false );
CHECK (is_linked(nM).port(0).asSrc(1).to(nB) == true );
CHECK (is_linked(nM).port(2).asSrc(0).to(nA) == true );
CHECK (is_linked(nM).port(2).asSrc(1).to(nA) == false );
CHECK (is_linked(nM).port(2).asSrc(0).to(nB) == false );
CHECK (is_linked(nM).port(2).asSrc(1).to(nB) == true );
CHECK (is_linked(nM).port(2).asSrc(1).to(nM) == false ); // never connected to itself
CHECK (is_linked(nM).port(0).asSrc(0).toLead(0) == true ); // Lead-#0 is Node-nA
CHECK (is_linked(nM).port(0).asSrc(1).toLead(0) == false );
CHECK (is_linked(nM).port(0).asSrc(0).toLead(1) == false );
CHECK (is_linked(nM).port(0).asSrc(1).toLead(1) == true );
CHECK (is_linked(nM).port(2).asSrc(0).toLead(0) == true );
CHECK (is_linked(nM).port(2).asSrc(1).toLead(0) == false );
CHECK (is_linked(nM).port(2).asSrc(0).toLead(1) == false );
CHECK (is_linked(nM).port(2).asSrc(1).toLead(1) == true );
// Again detailed port-to-port connections, this time limited by «source slot»
CHECK (is_linked(nM).port(0).asSrc(0).to(watch(nA).ports()[0]) == true ); // Node-nA, Port-0 connects as src-#0 to node-nA at Port-0
CHECK (is_linked(nM).port(0).asSrc(0).to(watch(nA).ports()[1]) == false); // ...and can thus not be connected to any other Port there
CHECK (is_linked(nM).port(0).asSrc(1).to(watch(nA).ports()[0]) == false);
CHECK (is_linked(nM).port(0).asSrc(1).to(watch(nA).ports()[1]) == false);
CHECK (is_linked(nM).port(0).asSrc(0).to(watch(nB).ports()[0]) == false);
CHECK (is_linked(nM).port(0).asSrc(0).to(watch(nB).ports()[1]) == false);
CHECK (is_linked(nM).port(0).asSrc(0).to(watch(nB).ports()[2]) == false);
CHECK (is_linked(nM).port(0).asSrc(1).to(watch(nB).ports()[0]) == true );
CHECK (is_linked(nM).port(0).asSrc(1).to(watch(nB).ports()[1]) == false);
CHECK (is_linked(nM).port(0).asSrc(1).to(watch(nB).ports()[2]) == false);
CHECK (is_linked(nM).port(2).asSrc(0).to(watch(nA).ports()[0]) == false);
CHECK (is_linked(nM).port(2).asSrc(0).to(watch(nA).ports()[1]) == true );
CHECK (is_linked(nM).port(2).asSrc(1).to(watch(nA).ports()[0]) == false);
CHECK (is_linked(nM).port(2).asSrc(1).to(watch(nA).ports()[1]) == false);
CHECK (is_linked(nM).port(2).asSrc(0).to(watch(nB).ports()[0]) == false);
CHECK (is_linked(nM).port(2).asSrc(0).to(watch(nB).ports()[1]) == false);
CHECK (is_linked(nM).port(2).asSrc(0).to(watch(nB).ports()[2]) == false);
CHECK (is_linked(nM).port(2).asSrc(1).to(watch(nB).ports()[0]) == false);
CHECK (is_linked(nM).port(2).asSrc(1).to(watch(nB).ports()[1]) == false);
CHECK (is_linked(nM).port(2).asSrc(1).to(watch(nB).ports()[2]) == true );
CHECK (is_linked(nM).port(2).asSrc(1).to(watch(nM).ports()[2]) == false); // never connected to itself
CHECK (is_linked(nM).port(0).asSrc(0).to(nA).atPort(0) == true );
CHECK (is_linked(nM).port(0).asSrc(0).to(nA).atPort(1) == false);
CHECK (is_linked(nM).port(0).asSrc(1).to(nA).atPort(0) == false);
CHECK (is_linked(nM).port(0).asSrc(1).to(nA).atPort(1) == false);
CHECK (is_linked(nM).port(0).asSrc(0).to(nB).atPort(0) == false);
CHECK (is_linked(nM).port(0).asSrc(0).to(nB).atPort(1) == false);
CHECK (is_linked(nM).port(0).asSrc(0).to(nB).atPort(2) == false);
CHECK (is_linked(nM).port(0).asSrc(1).to(nB).atPort(0) == true );
CHECK (is_linked(nM).port(0).asSrc(1).to(nB).atPort(1) == false);
CHECK (is_linked(nM).po