nodus 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +38 -0
- data/LICENSE.txt +20 -0
- data/OPERUM.md +8 -0
- data/README.md +383 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/dia.rb +29 -0
- data/doc/desc.md +191 -0
- data/doc/example.node +89 -0
- data/doc/nodes.rb +77 -0
- data/doc/pipe.svg +97 -0
- data/doc/pipe.txt +4 -0
- data/doc/pipe2.dot +49 -0
- data/doc/pipe2.svg +163 -0
- data/lib/VERSION +1 -0
- data/lib/extensions.rb +162 -0
- data/lib/flexhash.rb +175 -0
- data/lib/nodus.rb +77 -0
- data/lib/nodus/nodes.rb +160 -0
- data/lib/nodus/stream.rb +12 -0
- data/lib/nodus/token.rb +31 -0
- data/lib/nodus/version.rb +6 -0
- data/lib/proplist.rb +142 -0
- data/nodus.gemspec +106 -0
- data/spec.md +60 -0
- data/test/core/test_flexhash.rb +87 -0
- data/test/core/test_generator.rb +27 -0
- data/test/core/test_node.rb +103 -0
- data/test/core/test_proplist.rb +153 -0
- data/test/helper.rb +107 -0
- metadata +188 -0
data/lib/flexhash.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# Like a combination of HashWithIndifferentAccess, OpenStruct, plus it takes an integer and tries looking it up
|
2
|
+
# positionally. (bleh, like php's but a little more buggy. I know I know- but it's for a very specific purpose).
|
3
|
+
#
|
4
|
+
# Mostly cribbed from rubinius's ostruct: https://github.com/rubysl/rubysl-ostruct/blob/2.0/lib/rubysl/ostruct/ostruct.rb
|
5
|
+
# with just the [] access function changed and method_missing changed so it delegates unfound things to the underlying
|
6
|
+
# table.
|
7
|
+
|
8
|
+
class FlexHash
|
9
|
+
|
10
|
+
def self.[](*arr_const)
|
11
|
+
self.new(arr_const.flatten)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(constructor=nil)
|
15
|
+
@table = {}
|
16
|
+
if constructor.respond_to?(:each_pair)
|
17
|
+
constructor.each_pair{|k,v| self[k.to_sym] = v }
|
18
|
+
elsif constructor.respond_to?(:each_slice)
|
19
|
+
constructor.each_slice(2){|k,v| self[k.to_sym] = v }
|
20
|
+
elsif constructor != nil
|
21
|
+
raise ArgumentError, "cannot initialize flexhash with #{constructor}", caller(3)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize_copy(orig)
|
26
|
+
super
|
27
|
+
@table = @table.dup
|
28
|
+
@table.each_key { |key| new_flexhash_member(key) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_h
|
32
|
+
@table.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def each_pair
|
36
|
+
return to_enum __method__ unless block_given?
|
37
|
+
@table.each_pair { |p| yield p }
|
38
|
+
end
|
39
|
+
|
40
|
+
def marshal_dump
|
41
|
+
@table
|
42
|
+
end
|
43
|
+
|
44
|
+
def marshal_load(x)
|
45
|
+
@table = x
|
46
|
+
@table.each_key{|key| new_flexhash_member(key)}
|
47
|
+
end
|
48
|
+
|
49
|
+
def modifiable
|
50
|
+
begin
|
51
|
+
@modifiable = true
|
52
|
+
rescue
|
53
|
+
raise TypeError, "can't modify frozen #{self.class}", caller(3)
|
54
|
+
end
|
55
|
+
@table
|
56
|
+
end
|
57
|
+
protected :modifiable
|
58
|
+
|
59
|
+
def new_flexhash_member(name)
|
60
|
+
name = name.to_sym
|
61
|
+
unless respond_to?(name)
|
62
|
+
define_singleton_method(name) { @table[name] }
|
63
|
+
define_singleton_method("#{name}=") { |x| modifiable[name] = x }
|
64
|
+
end
|
65
|
+
name
|
66
|
+
end
|
67
|
+
protected :new_flexhash_member
|
68
|
+
|
69
|
+
def method_missing(mid, *args, &block)
|
70
|
+
mname = mid.id2name
|
71
|
+
len = args.length
|
72
|
+
if mname.chomp!('=')
|
73
|
+
if len != 1
|
74
|
+
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
|
75
|
+
end
|
76
|
+
modifiable[new_flexhash_member(mname)] = args[0]
|
77
|
+
elsif len == 0
|
78
|
+
res = @table[mid] || @table.send(mid, *args, &block)
|
79
|
+
res = FlexHash.new(res) if res.instance_of? Hash
|
80
|
+
res
|
81
|
+
else
|
82
|
+
res = @table.send(mid, *args, &block)
|
83
|
+
res = FlexHash.new(res) if res.instance_of? Hash
|
84
|
+
res
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def [](name)
|
89
|
+
if Integer === name
|
90
|
+
@table[name] || @table[@table.keys[name]]
|
91
|
+
else
|
92
|
+
res = @table[name] || @table[name.try(:to_sym)]
|
93
|
+
return res if res
|
94
|
+
res = @table.select{|k,v| name === k}
|
95
|
+
return res.values[0] if res.size == 1
|
96
|
+
res
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def []=(name, value)
|
101
|
+
modifiable[new_flexhash_member(name)] = value
|
102
|
+
end
|
103
|
+
|
104
|
+
def <<(kv)
|
105
|
+
name, value = kv
|
106
|
+
self[name] = value
|
107
|
+
end
|
108
|
+
|
109
|
+
def delete_field(name)
|
110
|
+
sym = name.to_sym
|
111
|
+
singleton_class.__send__(:remove_method, sym, "#{name}=")
|
112
|
+
@table.delete sym
|
113
|
+
end
|
114
|
+
|
115
|
+
InspectKey = :__inspect_key__ # :nodoc:
|
116
|
+
|
117
|
+
def inspect
|
118
|
+
str = "#<#{self.class}"
|
119
|
+
|
120
|
+
ids = (Thread.current[InspectKey] ||= [])
|
121
|
+
if ids.include?(object_id)
|
122
|
+
return str << ' ...>'
|
123
|
+
end
|
124
|
+
|
125
|
+
ids << object_id
|
126
|
+
begin
|
127
|
+
first = true
|
128
|
+
for k,v in @table
|
129
|
+
str << "," unless first
|
130
|
+
first = false
|
131
|
+
str << " #{k}=#{v.inspect}"
|
132
|
+
end
|
133
|
+
return str << '>'
|
134
|
+
ensure
|
135
|
+
ids.pop
|
136
|
+
end
|
137
|
+
end
|
138
|
+
alias :to_s :inspect
|
139
|
+
|
140
|
+
attr_reader :table # :nodoc:
|
141
|
+
protected :table
|
142
|
+
|
143
|
+
def ==(other)
|
144
|
+
return false unless other.kind_of?(OpenStruct)
|
145
|
+
@table == other.table
|
146
|
+
end
|
147
|
+
|
148
|
+
def eql?(other)
|
149
|
+
return false unless other.kind_of?(OpenStruct)
|
150
|
+
@table.eql?(other.table)
|
151
|
+
end
|
152
|
+
|
153
|
+
def hash
|
154
|
+
@table.hash
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Kind of like FlexHash, but assumes the elements of the array have a :name method, and allows duplicates
|
159
|
+
class FlexArray < Array
|
160
|
+
def [](k)
|
161
|
+
return super if Fixnum === k
|
162
|
+
res = self.find_all{|element| k === element.name || k.to_s === element.name.to_s}
|
163
|
+
return nil if res.blank?
|
164
|
+
res = res[0] if res.size == 1
|
165
|
+
res
|
166
|
+
end
|
167
|
+
|
168
|
+
def method_missing(mid, *args, &block)
|
169
|
+
mname = mid.id2name
|
170
|
+
len = args.length
|
171
|
+
res = self[mid]
|
172
|
+
return res unless res.blank?
|
173
|
+
super
|
174
|
+
end
|
175
|
+
end
|
data/lib/nodus.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'extensions'
|
3
|
+
require 'flexhash'
|
4
|
+
require 'mathn'
|
5
|
+
|
6
|
+
module Nodus
|
7
|
+
SRCDIR = File.dirname(__FILE__)
|
8
|
+
@_error_msg_map = {}
|
9
|
+
|
10
|
+
def self.def_exception(sym, msg, superclass=RuntimeError)
|
11
|
+
klass = Class.new(superclass)
|
12
|
+
Nodus.const_set(sym, klass)
|
13
|
+
@_error_msg_map[klass] = msg
|
14
|
+
end
|
15
|
+
|
16
|
+
def self._error_msg(klass) @_error_msg_map[klass] end
|
17
|
+
|
18
|
+
def self.const_missing(cname)
|
19
|
+
m = "nodus/#{cname.to_s.underscore}"
|
20
|
+
require m
|
21
|
+
klass = const_get(cname)
|
22
|
+
return klass if klass
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def error(klass, *args)
|
28
|
+
msg = Nodus._error_msg(klass)
|
29
|
+
msg ||= args.shift
|
30
|
+
raise klass, sprintf(*([msg] + args))
|
31
|
+
end
|
32
|
+
|
33
|
+
class Object
|
34
|
+
def try_dup()
|
35
|
+
self.dup
|
36
|
+
rescue
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Class
|
42
|
+
def save_as(klass_name)
|
43
|
+
Object.const_set(klass_name, self)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set class instance variable attributes, and see that the values get inherited by subclasses. Attribute readers are
|
47
|
+
# also set up for object instances that copy it from the class.
|
48
|
+
#
|
49
|
+
# The values are `dup`ed if possible and simply handed over otherwise (when inheriting and instantiating).
|
50
|
+
#
|
51
|
+
# Note that this seems to behave very differently than active-support's `class_attribute` method- which seems to use
|
52
|
+
# actual class-variables with all the problems they end up having. (of course I could have just been using
|
53
|
+
# active-support's wrong).
|
54
|
+
#
|
55
|
+
# The only safety that keeps these from affecting class instance variables where they shouldn't is the naming
|
56
|
+
# convention. Anything more clever than that and my ruby metaprogramming skills weren't up to snuff.
|
57
|
+
#
|
58
|
+
# Also, as you can surmise, it won't work if a class uses the `inherited` hook and fails to call super.
|
59
|
+
#
|
60
|
+
def class_attr_inheritable(attr_name, init_as=nil)
|
61
|
+
self.class_eval("def self.#{attr_name};@__cai__#{attr_name} end")
|
62
|
+
self.class_eval("def self.#{attr_name}=(v);@__cai__#{attr_name}=v end")
|
63
|
+
self.send("#{attr_name}=", init_as) unless init_as.nil?
|
64
|
+
self.class_eval("def #{attr_name};@#{attr_name} ||= self.class.#{attr_name}.try_dup end")
|
65
|
+
end
|
66
|
+
|
67
|
+
def inherited(subclass)
|
68
|
+
instance_variables.each do |v|
|
69
|
+
next unless v.to_s.starts_with?('@__cai__')
|
70
|
+
new_val = self.instance_variable_get(v).try_dup
|
71
|
+
subclass.instance_variable_set(v, new_val)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
require 'proplist'
|
77
|
+
require 'nodus/nodes'
|
data/lib/nodus/nodes.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
class Object
|
2
|
+
def kind_of_node?() false end
|
3
|
+
end
|
4
|
+
|
5
|
+
module Nodus
|
6
|
+
module Nodes
|
7
|
+
class Param < PropSet
|
8
|
+
default required: false, hidden: false
|
9
|
+
inverse visible: :hidden, required: :optional
|
10
|
+
def realize(val) self.default = val; self.hidden = true end
|
11
|
+
def realized?() self.hidden? || self.optional? || self.has_default? end
|
12
|
+
def realized()
|
13
|
+
return self.default if self.has_default?
|
14
|
+
error RuntimeError, "Parameter #{self.name} is required but hasn't had any values set." if self.required?
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class StreamPort < PropSet
|
20
|
+
# `| input: (operational<output-port[s]> | consumed [control]) x (optional | required)`
|
21
|
+
# `| output: ( operational<input-port[s]> | generated [control]) x (tap | primary)`
|
22
|
+
|
23
|
+
inverse input: :output
|
24
|
+
inverse optional: :required
|
25
|
+
inverse tap: :primary
|
26
|
+
inverse control: :stream
|
27
|
+
|
28
|
+
default optional: true
|
29
|
+
default primary: true
|
30
|
+
end
|
31
|
+
|
32
|
+
# I think we'll want the following to be completely disjoint:
|
33
|
+
# (common methods) ⊔ (node methods) ⊔ (parameter names) ⊔ (node names) ⊔ (stream-port names)
|
34
|
+
# This way we can do method_missing safely to specify params, nodes, ports, or anything else depending on the context.
|
35
|
+
#
|
36
|
+
# TODO: make nodes aware of their container? in order, perhaps, for them to ask the container who they should be
|
37
|
+
# connected to instead of the other way around?
|
38
|
+
#
|
39
|
+
class Node
|
40
|
+
class_attr_inheritable :title, nil
|
41
|
+
class_attr_inheritable :parameters, PropList.new(Param)
|
42
|
+
class_attr_inheritable :symmetric_ports, PropList.new(StreamPort)
|
43
|
+
class_attr_inheritable :consumed_ports, PropList.new(StreamPort)
|
44
|
+
class_attr_inheritable :generated_ports, PropList.new(StreamPort)
|
45
|
+
|
46
|
+
class_attr_inheritable :sub, FlexArray.new # Sub-nodes- mostly for later subclasses
|
47
|
+
class_attr_inheritable :channels, FlexArray.new # Binding pairs of inner nodes
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def kind_of_node?() true end
|
51
|
+
def param(param_name, *args)
|
52
|
+
arg_hash = (Hash === args.last ? args.pop : {})
|
53
|
+
arg_hash.merge!({name: param_name, node: self.title, node_type: self, node_name: self.name})
|
54
|
+
parameters << [param_name, args << arg_hash]
|
55
|
+
end
|
56
|
+
|
57
|
+
def compose(*args, &block)
|
58
|
+
Class.new(self) do |new_klass|
|
59
|
+
new_klass.on_compose(*args)
|
60
|
+
new_klass.instance_exec(new_klass, &block) if block_given?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
alias_method :[], :compose
|
64
|
+
|
65
|
+
# Override this at will with whatever parameters you want- just remember to call super, and with the right
|
66
|
+
# parameters
|
67
|
+
def on_compose(title)
|
68
|
+
error ArgumentError, "First argument to compose needs to be the symbolic title, not `#{title.inspect}`" unless title.kind_of? Symbol
|
69
|
+
self.title = title
|
70
|
+
end
|
71
|
+
|
72
|
+
# TODO:
|
73
|
+
# undefined_parameters() #=> tell what required parameters don't have defaults set
|
74
|
+
# complete?() #=> all required parameters & connections defined (well enough)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# --------------------------------- Instance --------------------------------------------------------
|
79
|
+
|
80
|
+
# Initialize will usually allow any parameters (/parameter overrides), and any object-level connection information
|
81
|
+
# required.
|
82
|
+
def initialize(*params)
|
83
|
+
# fill params with non-hash heads of args and then use any remaining hash to fill in more params
|
84
|
+
# runtime error if some required parameters are not set
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
class ConcurrentNode < Node
|
90
|
+
class << self
|
91
|
+
# Defined by aggregate of all parameters, input ports, and output ports (of all kinds). Probably automatically
|
92
|
+
# need their own naming conventions...
|
93
|
+
|
94
|
+
def on_compose(title, *sub_nodes)
|
95
|
+
super(title)
|
96
|
+
sub_nodes.each{|node| sub << node}
|
97
|
+
end
|
98
|
+
|
99
|
+
#def parameters
|
100
|
+
# sub.map{|s| s.parameters}
|
101
|
+
#end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
class Pipe < Node
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
# Given: enumerator or something that can be to_enum'ed
|
111
|
+
# Given: lambda or proc or block - params are gathered by reflection
|
112
|
+
# Given: class - assumed to be enumerable (or perhaps have a wrappable loop function?) params gathered via
|
113
|
+
# reflection on initialize.
|
114
|
+
# see http://stackoverflow.com/questions/4982630/trouble-yielding-inside-a-block-lambda when we get to lambdas etc.
|
115
|
+
#
|
116
|
+
class Generator < Node
|
117
|
+
class << self
|
118
|
+
attr_reader :kernel
|
119
|
+
|
120
|
+
def on_compose(title, kernel=nil, &block)
|
121
|
+
super(title)
|
122
|
+
@kernel = kernel || block
|
123
|
+
case @kernel
|
124
|
+
when Enumerator then @kernel = @kernel.lazy # No parameters
|
125
|
+
when Class
|
126
|
+
init_params = @kernel.instance_method(:initialize).parameters
|
127
|
+
init_params.each{|kind,pname| param(pname, (kind == :req ? :required : :optional))}
|
128
|
+
when Node
|
129
|
+
# TODO: Simply verify that the kernel has no input ports and create this thin wrapper around it...
|
130
|
+
# Although it still might make sense to warn that this is a senseless act? (unless it becomes
|
131
|
+
# necessary for some sorts of renaming etc.?)
|
132
|
+
error NotImplementedError
|
133
|
+
else
|
134
|
+
error ArgumentError, "Generator Nodes don't support #{kernel.inspect} as a kernel"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# given:
|
143
|
+
# parent_1 : p1, p2, p3
|
144
|
+
# parent_2 : p2, p3, p4
|
145
|
+
# parent_3 : p5
|
146
|
+
#
|
147
|
+
#
|
148
|
+
# parent_1__p1 -> valid
|
149
|
+
# parent_1__p2 -> valid
|
150
|
+
# ...
|
151
|
+
#
|
152
|
+
# p1, p4, p5 -> valid (maps to parent_1__p1 etc.)
|
153
|
+
# p2, p3 -> exception asks parent_1__... or parent_2__...?
|
154
|
+
# parent_1, parent_2 -> exception asks which property (p1, p2, or p3), or (p2, p3, p4)
|
155
|
+
# parent_3 -> valid (maps to parent_3__p5)
|
156
|
+
#
|
157
|
+
# doesn't solve the problem of nodes having the same name running in a concurrent composition (for example)!
|
158
|
+
#
|
159
|
+
# ability to rename params & ports as they get composed... (or as something else done while currying etc...)
|
160
|
+
#
|
data/lib/nodus/stream.rb
ADDED
data/lib/nodus/token.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
# - [ ] modification graph
|
5
|
+
# - [ ] timings in graph
|
6
|
+
# - [ ] current-data per thread/context
|
7
|
+
# - [ ] data appending
|
8
|
+
# - [ ] data freezing/locking per thread/context
|
9
|
+
#
|
10
|
+
|
11
|
+
module Nodus
|
12
|
+
def self.timestamp
|
13
|
+
# Use a better Process.clock_gettime time instead (not supported by rubinius yet)
|
14
|
+
Time.now
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: probably a token wrapper which designates the active data of the underlying token. This way the token can be
|
18
|
+
# the same token across all parallel streams, while each parallel stream will have it's own temporary view of it at
|
19
|
+
# each step...
|
20
|
+
|
21
|
+
class Token
|
22
|
+
attr_reader :seq_id, :stream, :timings
|
23
|
+
def initialize(stream, seq_id)
|
24
|
+
@stream = stream
|
25
|
+
@seq_id = seq_id
|
26
|
+
@timings = {generated: Nodus.timestamp}
|
27
|
+
@data = {}
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|