TODO use size_t, nd_t instead of int? modules nodes - most modules have a fixed number of nodes, each with a different name - some modules have node-lists or node-sets, which may be configured at build-time - some modules gain and lose nodes at run-time - each fixed node or node-group has a name - nodes are numbered within a node group node state - each node is allocated a non-negative integer `node descriptor', which is much like a file descriptor - at any time during execution, each node has three flags - push, pop and pull, which indicate that that operation may be performed without blocking. This parallels the select system call's can_read and can_write, but adds something of a wave/particle duality, which is pleasing! can_push - if this flag is set, it means that this node is ready to input an object using the `void push(int nd, void* obj)' method can_pop - if this flag is set, it means that this node has an object ready for ouput using the `void* pop(int nd)' method can_pull - if this flag is set, it means that this node is ready to accept a request for flow using the `void pull(int nd)' method - when a module receives a pull (or request), it will either propogate that request to other modules (like a wave), or produce an object on the specified node. The effect of pulling a node is that, some time later, an object may be popped from that node. - TODO I'm not sure whether `can_pull' should guarantee that an object will be produced, or not. What about exception conditions, like EOF? Should that propogate a special object, or signal on a different channel? - TODO If you call `pull', does that just pull one object, or does the pull continue for multiple objects? I think that the pull signal should only be sent once, and that the source modules should decide whether to push once or repeatedly. Sounds sensible? Do we need other flags and methods? `can_push' is ok for a stream merger, but for an adder, what about `must_push'! I guess that's what `pull' is for. Different `modes' of the producer modules might behave in different ways, with automatic re-pulling, etc. This is getting confused! stores and pumps - A store is a type of module that passively stores an object - you can push into it and pop from it at any time, it accepts but does not propogate pull. - A pump tries to contain an object at all times. It repeats a cycle pull, pop, push. This makes the stream sort of elastic (?) module state - data state - the full internal state of the module - may be discrete or continuous. - interface state - the combination of the module's nodes' states (can_push, can_pop and can_pull for each node). - modal state - equivalence partitions of data and interface state, with symbolic names. Okay - ready to go! This is going to be great! implementation (C++) node-descriptor - a smallish non-negative integer, will not change for a particular node, used as an index into the node vector and bit-vectors module - a module is a subclass of the abstract class `module' there are three bit-vectors, can_push, can_pop and can_pull, with one bit for each node-descriptor - TODO we need a method to dynamically get the names of nodes and node groups, and to look up? or to get the module's type, which can be used for this purpose? - there are twenty-two base methods - push, pop, pull, close on a nd void connect(int nd, object peer, int peer_nd) void connect(int nd, object peer, string peer_node) void connect(int nd, object peer, string peer_group, int peer_index) void push(int nd, void* obj) void* pop(int nd) void pull(int nd) void close(int nd) - push, pop, pull, close on a single node by name void connect(string node, object peer, int peer_nd) void connect(string node, object peer, string peer_node) void connect(string node, object peer, string peer_group, int peer_index) void push(string node, void* obj) void* pop(string node) void pull(string node) void close(string node) - push, pop, pull, close on a node in a group, by name and index void connect(string group, int index, object peer, int peer_nd) void connect(string group, int index, object peer, string peer_node) void connect(string group, int index, object peer, string peer_group, int peer_index) void push(string group, int index, void* obj) void* pop(string group, int index) void pull(string group, int index) void close(string group, int index) - count on a group, by name int count(string group) - there are two abstract methods - looking up node-descriptors by node-name, or by group-name and index virtual int nd(string node) virtual int nd(string node, int index) - the `dynamic' methods dispatch to the following `static' methods. Typically, the dispatch is implemented using either the module's nd lookup table, or the class's node name and group name hashtables. - there are four static methods for each single node, with these signatures: - push_N, pop_N, pull_N on a single node `N' void push_N(void* obj) void* pop_N() void pull_N() void close_N() - there are five static methods for each node group, with these signatures: - push_G, pop_G, pull_G on a node in a group, by index void push_G(int index, void* obj) void* pop_G(int index) void pull_G(int index) void close_G(int index) - count_G on a group int count_G() - the assert() macro is used liberally to ensure that the module really is in an appropriate state for a particular method call, and to validate parameters. Downcasts are done in assert. If NDEBUG is defined, no checking will be done, not even on node names. - TODO do we want to have indexed node-groups, node-hashes? I think this is unnecessary and probably a bad idea. Possibly might be good when we get to higher-order programming / meta-programming, but I doubt it. node - node name / number - node group (if any)