nodus 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|