ruleby 0.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.
@@ -0,0 +1,226 @@
1
+ #tokens
2
+ module Ruleby
3
+ module Core
4
+
5
+ # TODO eventually we want to refactor some of the match? and
6
+ # match_result logic out of these classes and into an AtomNode
7
+ # class. This will allow the Atoms to simply contain some attrs
8
+
9
+ class Atom
10
+ def initialize(tag, name, &block)
11
+ @tag = tag
12
+ @name = name
13
+ @proc = Proc.new(&block) if block_given?
14
+ @parent = nil # TODO move to AtomNode
15
+ end
16
+
17
+ attr :parent, true
18
+ attr_reader :name
19
+ attr_reader :tag
20
+ attr_reader :proc
21
+
22
+ end
23
+
24
+ # This kind of atom is used to match just a single, hard coded value.
25
+ # For example:
26
+ #
27
+ # a.name do |n| n == 'John' end
28
+ #
29
+ # So there are no references to other atoms.
30
+ class PropertyAtom < Atom
31
+
32
+ def match?(fact)
33
+ mr = MatchResult.new
34
+ temp = fact.object.send("#{@name}")
35
+ if @proc.call(temp)
36
+ mr.is_match = true
37
+ mr.fact_hash[@tag] = fact.id
38
+ mr.recency.push fact.recency
39
+ mr[@tag] = temp
40
+ end
41
+ return [mr]
42
+ end
43
+
44
+ def matches
45
+ @parent.obj_results
46
+ end
47
+
48
+ def ==(atom)
49
+ return atom != nil && @tag == atom.tag && @name == atom.name && @proc == atom.proc
50
+ end
51
+
52
+ def to_s
53
+ return 'tag='+@tag.to_s + '[' + matches.join(',') + ']'
54
+ end
55
+ end
56
+
57
+ # This kind of atom is used to match a class type. For example:
58
+ #
59
+ # p.has Person
60
+ #
61
+ # It is only used at the start of a pattern.
62
+ class TypeAtom < PropertyAtom
63
+ def match?(fact)
64
+ mr = MatchResult.new
65
+ temp = fact.object.send("#{@name}")
66
+ if @proc.call(temp)
67
+ mr.is_match = true
68
+ mr.recency.push fact.recency
69
+ mr.fact_hash[@tag] = fact.id
70
+ mr[@tag] = fact.object
71
+ end
72
+ return [mr]
73
+ end
74
+ end
75
+
76
+ # This kind of atom is used for matching a value that is a variable.
77
+ # For example:
78
+ #
79
+ # a.name references => :your_name do |n,yn| n == yn end
80
+ #
81
+ # The expression for this atom depends on some other atom.
82
+ class ReferenceAtom < Atom
83
+
84
+ def initialize(tag, name, vars, &block)
85
+ super(tag, name, &block)
86
+ @vars = vars # list of referenced variable names
87
+ @var_atoms = Hash.new # will be set when rule is asserted
88
+ end
89
+
90
+ attr_reader :vars
91
+ attr :var_atoms, true
92
+
93
+ def match?(fact)
94
+ local_matches = Array.new
95
+ val = fact.object.send("#{@name}")
96
+ @vars.each do |v|
97
+ # TODO we don't really need to iterate over all the @var_atoms, just
98
+ # the one that is at the top of the reference chain (i.e. the one that
99
+ # has references to all of this atoms other references.
100
+ @var_atoms[v].matches.each do |match|
101
+ args = [val]
102
+ args.push match.variables[v]
103
+ i = 0; while i < @vars.length
104
+ if match.key? @vars[i]
105
+ args.push match.variables[@vars[i]]
106
+ i+=1
107
+ else
108
+ i = @vars.length + 1
109
+ end
110
+ end
111
+ break if i > @vars.length
112
+ if @proc.call(args)
113
+ m = MatchResult.new(match.variables.clone, true, match.fact_hash.clone, match.recency)
114
+ m.recency.push fact.recency
115
+ m.fact_hash[@tag] = fact.id
116
+ m.variables[@tag] = val
117
+ local_matches.push(m)
118
+ end
119
+ end
120
+ end
121
+ return local_matches.uniq
122
+ end
123
+
124
+ def matches
125
+ return @parent.ref_results(@tag)
126
+ end
127
+
128
+ def ==(atom)
129
+ return atom.kind_of?(ReferenceAtom) && @proc == atom.proc && @tag == atom.tag && @vars == atom.vars
130
+ end
131
+
132
+ def to_s
133
+ return 'tag='+@tag.to_s + '[' + matches.join(',') + ']'
134
+ end
135
+ end
136
+
137
+ class MatchResult
138
+
139
+ # TODO this class needs to be cleaned up for that we don't have a bunch of
140
+ # properties. Instead, maybe it sould have a list of facts.
141
+ def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[])
142
+ @variables = variables
143
+
144
+ # a list of recencies of the facts that this matchresult depends on.
145
+ @recency = recency
146
+
147
+ # notes where this match result is from a NotPattern or ObjectPattern
148
+ # TODO this isn't really needed anymore. how can we get rid of it?
149
+ @is_match = is_match
150
+
151
+ # a hash of fact.ids that each tag corresponds to
152
+ # QUESTION what is better: this or the @backward_hash in MatchContext?
153
+ # Right now these two are somewhat redundate.
154
+ @fact_hash = fact_hash
155
+ end
156
+ def []=(sym, object)
157
+ @variables[sym] = object
158
+ end
159
+ def [](sym)
160
+ return @variables[sym]
161
+ end
162
+ def fact_ids
163
+ return fact_hash.values.uniq
164
+ end
165
+ attr :variables, true
166
+ attr :is_match, true
167
+ attr :fact_hash, true
168
+ attr :resolved, true
169
+ attr :recency, true
170
+ def ==(match)
171
+ return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash
172
+ end
173
+
174
+ def key?(m)
175
+ return @variables.key?(m)
176
+ end
177
+
178
+ def keys
179
+ return @variables.keys
180
+ end
181
+
182
+ def update(mr)
183
+ @recency = @recency | mr.recency
184
+ @is_match = mr.is_match
185
+ @variables = @variables.update mr.variables
186
+
187
+ # QUESTION why the heck is does this statement work instead of the
188
+ # commented one below it??
189
+ @fact_hash = mr.fact_hash.update @fact_hash
190
+ #@fact_hash = @fact_hash.update mr.fact_hash
191
+
192
+ return self
193
+ end
194
+
195
+ def dup
196
+ dup_mr = MatchResult.new
197
+ dup_mr.recency = @recency.clone
198
+ dup_mr.is_match = @is_match
199
+ dup_mr.variables = @variables.clone
200
+ dup_mr.fact_hash = @fact_hash.clone
201
+ return dup_mr
202
+ end
203
+
204
+ def clear
205
+ @variables = {}
206
+ @fact_hash = {}
207
+ @recency = []
208
+ end
209
+
210
+ def delete(tag)
211
+ @variables.delete(tag)
212
+ @fact_hash.delete(tag)
213
+ end
214
+
215
+ def to_s
216
+ s = '#MatchResult('
217
+ s = s + 'f)(' unless @is_match
218
+ s = s + object_id.to_s+')('
219
+ @variables.each do |key,value|
220
+ s += "#{key}=#{value}/#{@fact_hash[key]}, "
221
+ end
222
+ return s + ")"
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,211 @@
1
+ #core core
2
+ require 'core/atoms'
3
+ require 'core/patterns'
4
+ require 'core/utils'
5
+ require 'core/nodes'
6
+
7
+ module Ruleby
8
+ module Core
9
+ class Action
10
+ def initialize(&block)
11
+ @name = nil
12
+ @proc = Proc.new(&block) if block_given?
13
+ @priority = 0
14
+ end
15
+
16
+ attr_accessor :priority
17
+ attr_accessor :name
18
+ attr_reader :matches
19
+
20
+ def fire(r, match)
21
+ @proc.call(r, match)
22
+ end
23
+
24
+ def ==(a2)
25
+ return @name == a2.name
26
+ end
27
+ end
28
+
29
+ class Activation
30
+ def initialize(action, match)
31
+ @action = action
32
+ @match = match
33
+ @match.recency.sort!
34
+ @match.recency.reverse!
35
+ @counter = 0
36
+ end
37
+
38
+ attr_reader :action, :match
39
+ attr_accessor :counter
40
+
41
+ def fire(r)
42
+ @action.fire r, @match
43
+ end
44
+
45
+ def <=>(a2)
46
+ return @counter <=> a2.counter if @counter != a2.counter
47
+ return @action.priority <=> a2.action.priority if @action.priority != a2.action.priority
48
+
49
+ # NOTE in order for this to work, the array must be reverse sorted
50
+ i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1
51
+ i += 1
52
+ end
53
+ return @match.recency[i] <=> a2.match.recency[i]
54
+ end
55
+
56
+ def ==(a2)
57
+ return a2 != nil && @action == a2.action && @match == a2.match
58
+ end
59
+
60
+ def to_s
61
+ return "[#{@action.name}-#{object_id}|#{@counter}|#{@action.priority}|#{@match.recency.join(',')}|#{@match.to_s}] "
62
+ end
63
+ end
64
+
65
+ class Rule
66
+ def initialize(name, pattern=nil, action=nil, priority=0)
67
+ @name = name
68
+ @pattern = pattern
69
+ @action = action
70
+ @priority = priority
71
+ end
72
+
73
+ attr_accessor:pattern
74
+ attr_reader:action, :name
75
+
76
+ def when=(pattern)
77
+ @pattern = pattern
78
+ end
79
+
80
+ def then=(action)
81
+ @action = action
82
+ @action.name = @name
83
+ @action.priority = @priority
84
+ end
85
+
86
+ def priority
87
+ return @priority
88
+ end
89
+
90
+ def priority=(p)
91
+ @priority = p
92
+ @action.priority = @priority
93
+ end
94
+ end
95
+
96
+ class Fact
97
+ def initialize(object, token)
98
+ @token = token
99
+ @object = object
100
+ end
101
+
102
+ attr :token, true
103
+ attr :recency, true
104
+ attr_reader :object
105
+
106
+ def id
107
+ return object.object_id
108
+ end
109
+
110
+ def ==(fact)
111
+ return fact != nil && fact.id == id
112
+ end
113
+
114
+ def to_s
115
+ return "[Fact |#{@recency}|#{@object.to_s}]"
116
+ end
117
+ end
118
+
119
+ class RulebyConflictResolver
120
+ def resolve(agenda)
121
+ return agenda.sort
122
+ end
123
+ end
124
+
125
+ class WorkingMemory
126
+ def initialize
127
+ @recency = 0
128
+ @facts = Array.new
129
+ end
130
+
131
+ attr_reader :facts
132
+
133
+ def assert_fact(fact)
134
+ raise 'The fact asserted cannot be nil!' unless fact.object
135
+ if (fact.token == :plus)
136
+ fact.recency = @recency
137
+ @recency += 1
138
+ @facts.push fact
139
+ return fact
140
+ else #if (fact.token == :minus)
141
+ i = @facts.index(fact)
142
+ raise 'The fact to remove does not exist!' unless i
143
+ existing_fact = @facts[i]
144
+ @facts.delete_at(i)
145
+ existing_fact.token = fact.token
146
+ return existing_fact
147
+ end
148
+ end
149
+
150
+ def print
151
+ puts 'WORKING MEMORY:'
152
+ @facts.each do |fact|
153
+ puts " #{fact.object} - #{fact.id} - #{fact.recency}"
154
+ end
155
+ end
156
+ end
157
+
158
+ class Engine
159
+ def initialize()
160
+ @root = nil
161
+ @working_memory = WorkingMemory.new
162
+ @conflict_resolver = RulebyConflictResolver.new
163
+ end
164
+
165
+ def assert_fact(fact)
166
+ wm_fact = @working_memory.assert_fact fact
167
+ @root.assert_fact wm_fact if @root != nil
168
+ end
169
+
170
+ def assert_rule(rule)
171
+ @root = RootNode.new(@working_memory) if @root == nil
172
+ @root.assert_rule rule
173
+ end
174
+
175
+ def match(agenda=@root.match, used_agenda=[], activation_counter=0)
176
+ agenda = @conflict_resolver.resolve agenda
177
+ if(agenda && agenda.length > 0)
178
+ activation = agenda.pop
179
+ used_agenda.push activation
180
+ activation.fire self
181
+
182
+ new_agenda = @root.match
183
+
184
+ # HACK the following is a workaround. This problem would best be
185
+ # solved by working this into the nodes themselves.
186
+ new_agenda.each do |a|
187
+ used = false
188
+ used_agenda.each do |used_activation|
189
+ used = true if used_activation.object_id == a.object_id
190
+ end
191
+
192
+ # BUG we are comparing against the current agenda, but we may need to
193
+ # compare against all activations that have existed...
194
+ if (agenda.index(a) == nil) && (!used)
195
+ a.counter = activation_counter+1
196
+ agenda.push a
197
+ end
198
+ end
199
+ agenda.delete_if {|a| new_agenda.index(a) == nil}
200
+
201
+ match(agenda, used_agenda, activation_counter+1)
202
+ end
203
+ end
204
+
205
+ def print
206
+ @working_memory.print
207
+ @root.print
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,684 @@
1
+ module Ruleby
2
+ module Core
3
+
4
+ # This acts as a base class for all Nodes that wrap a pattern and
5
+ # output to one or more other Nodes.
6
+ class Node
7
+
8
+ def initialize(pattern)
9
+ # the pattern that this node wraps
10
+ @pattern = pattern
11
+
12
+ # The nodes that this node outputs to (ones that are below it in the
13
+ # network). Currently, there will only be a single value in each list
14
+ # but in the future, out network will be optimize to remove repeated
15
+ # nodes. Thus each node can output to multiple nodes.
16
+ @out_nodes = []
17
+ end
18
+
19
+ attr_reader :pattern
20
+ attr_reader :out_nodes
21
+
22
+ # This method is called when a new MatchResult is added to the system. It
23
+ # propagates the new MR by invoking the assert method on all nodes that are
24
+ # below this Node in the network.
25
+ def propagate_assert(match_result, fact)
26
+ @out_nodes.each do |out_node|
27
+ out_node.assert(self, match_result, fact)
28
+ end
29
+ end
30
+
31
+ # This method is called when an existing MatchResult is remvoed from the
32
+ # system. It propagates the removal by invoking the retract method on all
33
+ # nodes that are below this Node in the network.
34
+ def propagate_retract(fact)
35
+ @out_nodes.each do |out_node|
36
+ out_node.retract(self, fact)
37
+ end
38
+ end
39
+
40
+ # This method is used to progagate a new MatchResult. The difference
41
+ # in this method is that it propagates MatchResults that were
42
+ # generated by a NotNode
43
+ def propagate_assert_neg(neg_match_result, fact)
44
+ @out_nodes.each do |out_node|
45
+ out_node.assert_neg(self, neg_match_result, fact)
46
+ end
47
+ end
48
+
49
+ # This method is used to progagate the removal of a MatchResult. The
50
+ # difference in this method is that it removes MatchResults that were
51
+ # generated by a NotNode
52
+ def propagate_retract_neg(fact)
53
+ @out_nodes.each do |out_node|
54
+ out_node.retract_neg(self, fact)
55
+ end
56
+ end
57
+
58
+ # This method determines if all common tags have equal values. If any
59
+ # values are not equal then the method returns false.
60
+ def resolve(mr1, mr2)
61
+ mr1.variables.each_key do |t1|
62
+ mr2.variables.each_key do |t2|
63
+ if t2 == t1 && mr1.fact_hash[t1] != mr2.fact_hash[t2]
64
+ return false
65
+ end
66
+ end
67
+ end
68
+ return true
69
+ end
70
+
71
+ # Determines if a given MatchResult is unmatched by the any of the given
72
+ # NotMatchResults.
73
+ # mr - a MatchResult
74
+ # nmrs - a list of MatchResults from a NotNode
75
+ def matches_not?(mr,nmrs)
76
+ nmrs.each do |nmr|
77
+ return true if resolve(mr,nmr)
78
+ end
79
+ return false
80
+ end
81
+ end
82
+
83
+ # This class acts as the root-node of the network. It contains the logic
84
+ # for building the node-network from a set of rules, and updating it
85
+ # according to the working memory
86
+ class RootNode
87
+
88
+ def initialize(working_memory)
89
+ @working_memory = working_memory
90
+
91
+ # we need to use our own data structure (NodeNetworkArray) for the most
92
+ # efficient indexing of nodes. Also, it creates some mappings that will
93
+ # be used later on to avoid repeatedly iterating over lists.
94
+ @object_nodes = NodeNetworkArray.new
95
+
96
+ @join_nodes = Array.new
97
+ @terminal_nodes = Array.new
98
+ end
99
+
100
+ # This method is invoked when a new rule is added to the system. The
101
+ # rule is processed and the appropriate nodes are added to the network.
102
+ def assert_rule(rule)
103
+ pattern = rule.pattern
104
+ terminal_node = TerminalNode.new rule
105
+ node = build_network(pattern, terminal_node)
106
+ terminal_node.parent_node = node # only used to print the network
107
+ @terminal_nodes.push terminal_node
108
+ end
109
+
110
+ # This method builds the network by starting at the bottom and recursively
111
+ # working its way to the top. The recursion goes up the left side of the
112
+ # tree first (depth first... but our tree is up-side-down).
113
+ # pattern - the pattern to process (Single or Composite)
114
+ # out_node - the node that will be below the new node in the network
115
+ # side - if the out_node is a JoinNode, this marks the side of the new node
116
+ # Returns a new node in the network that wraps the given pattern and
117
+ # is above (i.e. it outputs to) the given node.
118
+ def build_network(pattern, out_node, side=nil)
119
+ if (pattern.kind_of?(ObjectPattern))
120
+ return create_object_node(pattern, out_node, side)
121
+ else
122
+ join_node = create_join_node(pattern, out_node, side)
123
+ build_network(pattern.left_pattern, join_node, :left)
124
+ build_network(pattern.right_pattern, join_node, :right)
125
+ return join_node
126
+ end
127
+ end
128
+ private:build_network
129
+
130
+ # Creates an ObjectNode, puts it at the top of the network, and stores
131
+ # the node below it into its memory. It also checks the working memory to
132
+ # see if its conditions are satisfied by the exisiting facts.
133
+ # pattern - the ObjectPattern that the created node wraps
134
+ # out_node - the Node that this pattern is directly above in the network
135
+ # side - if the out_node is a JoinNode, this marks the side of the new node
136
+ def create_object_node(pattern, out_node, side)
137
+ obj_node = nil
138
+ if (pattern.kind_of?(NotPattern))
139
+ obj_node = NotNode.new(pattern)
140
+ else
141
+ obj_node = ObjectNode.new(pattern)
142
+ end
143
+
144
+ if side==:left
145
+ out_node.left_input = obj_node
146
+ elsif side==:right
147
+ out_node.right_input = obj_node
148
+ end
149
+
150
+ # TODO fix this opmtimization so it works
151
+ #index = @object_nodes.has_node(obj_node)
152
+ #if (index == nil)
153
+ @object_nodes.add(obj_node)
154
+ #else
155
+ #obj_node = @object_nodes[obj_node.pattern.head,index]
156
+ # TODO maybe create and push new "alias" node where the @tags
157
+ # do not have to be the same, but the patterns are the same
158
+ #end
159
+
160
+ obj_node.out_nodes.push out_node
161
+ compare_node_to_wm(obj_node)
162
+ return obj_node
163
+ end
164
+ private:create_object_node
165
+
166
+ # Creates a JoinNode, puts it at the middle of the network, and stores
167
+ # the node below it into its memory.
168
+ # pattern - the Pattern that the created node wraps
169
+ # out_node - the Node that this pattern is directly above in thw network
170
+ # side - if the out_node is a JoinNode, this marks the side of the new node
171
+ def create_join_node(pattern, out_node, side)
172
+ join_node = JoinNode.new(pattern)
173
+
174
+ if side==:left
175
+ out_node.left_input = join_node
176
+ elsif side==:right
177
+ out_node.right_input = join_node
178
+ end
179
+
180
+ # TODO opmtimization the join_nodes list (just like object_nodes)
181
+ @join_nodes.push(join_node)
182
+
183
+ join_node.out_nodes.push out_node
184
+ return join_node
185
+ end
186
+ private:create_join_node
187
+
188
+ # When a new fact is added to working memory, or an existing one is removed
189
+ # this method is called. It finds any nodes that depend on it, and updates
190
+ # them accordingly.
191
+ def assert_fact(fact)
192
+ @object_nodes.each fact.object.class do |object_node|
193
+ altered = false
194
+ if fact.token == :plus
195
+ altered = object_node.assert fact
196
+ else
197
+ altered = object_node.retract fact
198
+ end
199
+
200
+ # BUG this may cause there to be multiple MatchResults for the same
201
+ # fact. It will be as if there were 2 of the same fact in working
202
+ # memory. Right now it is okay because we are removing duplicate
203
+ # MatchResults. But this means we cannot have duplicate facts in
204
+ # working memory. The following WILL need to be addressed at
205
+ # some point.
206
+ #
207
+ # QUESTION a possible solution would be to pass an 'is_new' parameter to
208
+ # the refresh_node method. This param could represent that we are
209
+ # refreshing (as opposed to calling it from assert_rule - which would
210
+ # be the first time).
211
+ if altered
212
+ check_references(object_node)
213
+ end
214
+ end
215
+ end
216
+
217
+ def check_references(object_node)
218
+ object_node.referenced_by.each do |ref_node|
219
+ # HACK this is a very costly operation because we will have to iterate
220
+ # over ALL facts in working memory to see if ANY nodes were affected
221
+ # by the fact asserted/retracted here. So patterns using ReferenceAtoms
222
+ # become very expensive.
223
+ altered = compare_node_to_wm(ref_node)
224
+
225
+ # call this method recursively until we reach a node that is not
226
+ # referenced by any other nodes
227
+ if altered
228
+ check_references(ref_node)
229
+ end
230
+ end
231
+ end
232
+
233
+ # This method is used to update each ObjectNode in the given list based on
234
+ # the facts in working memory. It can be a costly operation because it
235
+ # iterates over EVERY fact in working memory. It should be used only when
236
+ # needed.
237
+ def compare_node_to_wm(obj_node)
238
+ altered = false
239
+ facts = @working_memory.facts
240
+ if (facts.empty? && obj_node.kind_of?(NotNode))
241
+ # HACK its kind of lame to have to do this here, maybe the NotNode
242
+ # should have this value by default
243
+ obj_node.assert_input_true
244
+ else
245
+ # check working memory to see if this node is satisfied by any fact(s)
246
+ facts.each do |fact|
247
+ inner_altered = obj_node.assert fact
248
+ altered = true if inner_altered
249
+ end
250
+ end
251
+ return altered
252
+ end
253
+
254
+ # When invoked, this method returns a list of all Action|MatchContext pairs
255
+ # as Activations. The list is generated when facts and rules are asserted,
256
+ # so no comparisions are done here (i.e. no Backward Chaining).
257
+ def match
258
+ agenda = Array.new
259
+ @terminal_nodes.each do |node|
260
+ if (node.satisfied?)
261
+ agenda.concat node.activations.values.values
262
+ end
263
+ end
264
+ return agenda
265
+ end
266
+
267
+ def print
268
+ puts 'NETWORK:'
269
+ @terminal_nodes.each do |node|
270
+ node.print(' ')
271
+ end
272
+ end
273
+ end
274
+
275
+ # This class represents a one-input node. It essenatially acts as a wrapper
276
+ # for an ObjectPattern, and provides the logic for matching facts.
277
+ class ObjectNode < Node
278
+ def initialize(pattern)
279
+ super
280
+
281
+ # a list of Nodes that reference this node. This will be used so that we
282
+ # can update them if this node get some new matches (or loses matches).
283
+ @referenced_by = []
284
+
285
+ # The atoms need a handle to this node.
286
+ @pattern.atoms.each { |a| a.parent = self }
287
+
288
+ # In this hash, the key is a fact.id, and each value is a list of
289
+ # MatchResults. Each MR is from a node that this node references.
290
+ @ref_results_hash = {}
291
+
292
+ # In this hash, the key is a fact.id, and each value is a single
293
+ # MatchResult. The MRs represent the match for each fact.
294
+ @obj_results_hash = {}
295
+ end
296
+
297
+ attr:referenced_by,true
298
+
299
+ def satisfied?
300
+ return !@obj_results_hash.empty?
301
+ end
302
+
303
+ def obj_results
304
+ return @obj_results_hash.values
305
+ end
306
+
307
+ def ref_results(tag)
308
+ list = []
309
+ @ref_results_hash.each_value do |ref_result|
310
+ list.concat ref_result[tag] if ref_result[tag] != ref_result.default
311
+ end
312
+ return list
313
+ end
314
+
315
+ # This method initiates the matching of this pattern to the given fact
316
+ # (specificily to the object it wraps). If the pattern matches the object
317
+ # then one or more MatchResults will be created and added to the system.
318
+ # returns:boolean - true if this node was altered by the assertion
319
+ def assert(fact)
320
+ obj_result = MatchResult.new
321
+ ref_results = {}
322
+ results = [MatchResult.new]
323
+
324
+ @pattern.atoms.each do |atom|
325
+ r = atom.match? fact
326
+ if r.empty? || !r[0].is_match
327
+ return false
328
+ else
329
+ # Build the MatchResult that will be added to each atom
330
+ # after we have gone through all atoms. We must wait
331
+ # till after we check all atoms because we only add it
332
+ # if ALL atoms have a match for this object.
333
+ obj_result[atom.tag] = r[0][atom.tag]
334
+ obj_result.is_match = r[0].is_match
335
+ obj_result.fact_hash = obj_result.fact_hash.update r[0].fact_hash
336
+ obj_result.recency = obj_result.recency | r[0].recency
337
+
338
+ ref_results[atom.tag] = []
339
+ results = build_results results, ref_results, r, atom
340
+ end
341
+ ref_results[atom.tag].each do |ref_result|
342
+ # update each ReferenceAtom's MatchResult with the core result for this fact
343
+ ref_result.update obj_result
344
+ end
345
+ end
346
+
347
+ @ref_results_hash[fact.id] = ref_results
348
+ @obj_results_hash[fact.id] = obj_result
349
+
350
+ # HACK delete repeated MatchResults. Instead of doing this
351
+ # we need to be sure that every MatchResult added is unique (or is
352
+ # intended to be a duplicate).
353
+ results.each do |r1|
354
+ results.delete_if do |r2|
355
+ r1 == r2 && r1.object_id != r2.object_id
356
+ end
357
+ end
358
+
359
+ results.each do |mr|
360
+ propagate_assert mr, fact
361
+ end
362
+ return !results.empty?
363
+ end
364
+
365
+ # This method is called when a fact is removed from the system. If the
366
+ # patterned contained in the node was matched to the given fact, then we
367
+ # removed those MatchResults from our local cache, and propagate the change.
368
+ # returns:boolean - true if this node was altered by the retraction
369
+ def retract(fact)
370
+ @ref_results_hash.delete(fact.id)
371
+ result = @obj_results_hash.delete(fact.id)
372
+ if result != @obj_results_hash.default
373
+ propagate_retract fact
374
+ return true
375
+ end
376
+ return false
377
+ end
378
+
379
+ # This internal method is used to build the result set for matching fact-
380
+ # pattern combination.
381
+ def build_results(results, ref_results, r, atom)
382
+ inner_results = []
383
+ r.each do |each_r|
384
+ # Update each MatchResult already in the list with this MatchResult
385
+ # if the two resolve. Then add the result to the list for this atom.
386
+ results.each do |result|
387
+ if resolve(result, each_r)
388
+ ref_results[atom.tag].push each_r
389
+ mr = result.dup
390
+ mr.update(each_r)
391
+ inner_results.push mr
392
+ end
393
+ end
394
+ end
395
+ return inner_results
396
+ end
397
+ private:build_results
398
+
399
+ def ==(node)
400
+ if (node.kind_of?(ObjectNode))
401
+ return @pattern == node.pattern
402
+ end
403
+ end
404
+
405
+ def print(tab)
406
+ puts tab + to_s + " (#{satisfied?}) #{@pattern} -> #{@out_nodes}"
407
+ end
408
+ end
409
+
410
+ # This class represents a one-input node. It essenatially acts as a wrapper
411
+ # for an NotPattern, and provides the logic for matching facts. This class
412
+ # is similar to ObjectNode (and in fact extends it), but some of the matching
413
+ # logic is different. This is because when this matches facts that need to
414
+ # not exist in working memory.
415
+ class NotNode < ObjectNode
416
+ def satisfied?
417
+ return true # there is always something not there...
418
+ end
419
+
420
+ # This method initiates the matching of this pattern to the given fact
421
+ # (really to the object it wraps). If the pattern matches the object
422
+ # then one or more MatchResults will be created and added to the system.
423
+ # returns:boolean - false because no nodes can reference a NotNode
424
+ def assert(fact)
425
+ ref_results = {}
426
+ results = [MatchResult.new]
427
+
428
+ @pattern.atoms.each do |atom|
429
+ r = atom.match? fact
430
+ if r.empty? || !r[0].is_match
431
+ # QUESTION do we need to add this for every unmatched fact?
432
+ propagate_assert MatchResult.new({},true), fact
433
+ return false
434
+ else
435
+ ref_results[atom.tag] = []
436
+ results = build_results results, ref_results, r, atom
437
+ end
438
+ end
439
+
440
+ @ref_results_hash[fact.id] = ref_results
441
+
442
+ # HACK delete repeated MatchResults. Instead of doing this
443
+ # we need to be sure that every MatchResult added is unique.
444
+ results.each do |r1|
445
+ results.delete_if do |r2|
446
+ r1 == r2 && r1.object_id != r2.object_id
447
+ end
448
+ end
449
+
450
+ results.each do |mr|
451
+ # TODO we need to extend the atoms so that NotNodes's results don't
452
+ # have the variables that we are removing below
453
+ mr.keys.each do |key|
454
+ delete = true
455
+ @pattern.atoms.each do |a|
456
+ if (a.kind_of?(ReferenceAtom) && a.vars.index(key) != nil)
457
+ delete = false
458
+ end
459
+ end
460
+ mr.delete key if delete
461
+ end
462
+
463
+ propagate_assert_neg mr, fact
464
+ end
465
+ return false # nothing can reference a NotNode... easy
466
+ end
467
+
468
+ # This method is called when a fact is removed from the system. If the
469
+ # patterned contained in the node was matched to the given fact, then we
470
+ # removed those MatchResults from our local cache, and propagate the change.
471
+ # returns:boolean - always false because no other nodes will ever care
472
+ def retract(fact)
473
+ unless @ref_results_hash.delete(fact.id) == @ref_results_hash.default
474
+ propagate_retract_neg fact
475
+ end
476
+ return false # nothing can reference a NotNode... easy
477
+ end
478
+
479
+ # This method forces the node to be true (regardless of the facts in
480
+ # working memory). This is need when the working memory is empty.
481
+ def assert_input_true
482
+ propagate_assert MatchResult.new({},true), nil
483
+ end
484
+
485
+ def print(tab)
486
+ puts tab + to_s + " (#{satisfied?}) #{@pattern} -> #{@out_nodes}"
487
+ end
488
+ end
489
+
490
+ # This class represents a two-input node. It acts as a wrapper for a
491
+ # composite pattern, and keeps a memory of whether or not the left and
492
+ # right values have been satisfied.
493
+ class JoinNode < Node
494
+
495
+ # the 'pattern' parameter denotes the boolean operator that this class
496
+ # will use to compute if this join_node has been satisfied.
497
+ def initialize(pattern)
498
+ super pattern
499
+ @left_memory = MatchContext.new
500
+ @right_memory = MatchContext.new
501
+ @local_memory = MatchContext.new
502
+ @not_memory = MatchContext.new
503
+ @left_input = nil
504
+ @right_input = nil
505
+ end
506
+
507
+ attr :left_input, true
508
+ attr :right_input, true
509
+ attr :local_memory, true
510
+
511
+ def satisfied?
512
+ return @local_memory.satisfied?
513
+ end
514
+
515
+ def assert(in_node, mr, fact)
516
+ left = []; right = []
517
+ if (in_node.object_id == @left_input.object_id)
518
+ return if @left_memory.contains?(mr)
519
+ @left_memory.add mr.fact_ids, mr
520
+ left = [mr]
521
+ right = @right_memory.match_results
522
+ elsif (in_node.object_id == @right_input.object_id)
523
+ return if @right_memory.contains?(mr)
524
+ @right_memory.add mr.fact_ids, mr
525
+ left = @left_memory.match_results
526
+ right = [mr]
527
+ else
528
+ return
529
+ end
530
+ list = filter_match_results(left, right)
531
+ list.each do |new_mr|
532
+ # QUESTION can we just do this inside of filter_match_results?
533
+ @local_memory.add new_mr.fact_ids, new_mr
534
+ propagate_assert new_mr, fact
535
+ end
536
+ end
537
+
538
+ def retract(in_node, fact)
539
+ if in_node.object_id == @left_input.object_id
540
+ @left_memory.remove fact.id
541
+ elsif in_node.object_id == @right_input.object_id
542
+ @right_memory.remove fact.id
543
+ else
544
+ return
545
+ end
546
+ @local_memory.remove fact.id
547
+ propagate_retract fact
548
+ end
549
+
550
+ def assert_neg(in_node, nmr, fact)
551
+ return if @not_memory.contains?(nmr)
552
+ @not_memory.add nmr.fact_ids, nmr
553
+ @local_memory.delete_if do |mr|
554
+ matches_not?(mr,[nmr])
555
+ end
556
+ propagate_assert_neg nmr, fact
557
+ end
558
+
559
+ def retract_neg(in_node, fact)
560
+ result = @not_memory.remove fact.id
561
+ return if result == @not_memory.default
562
+
563
+ # TODO keep a cache of MatchResults that were left out because of
564
+ # certain not_match_results. Hash these by the fact.id of those NMRs.
565
+ list = filter_match_results(@left_memory.match_results, @right_memory.match_results)
566
+ list.each do |mr|
567
+ unless @local_memory.contains?(mr)
568
+ @local_memory.push mr
569
+ propogate_assert mr, nil
570
+ end
571
+ end
572
+ propagate_retract_neg fact
573
+ end
574
+
575
+ def ==(node)
576
+ if (node.kind_of?(JoinNode))
577
+ return @pattern == node.pattern
578
+ end
579
+ end
580
+
581
+ def print(tab)
582
+ puts tab + "JoinNode:#{satisfied?}(#{@left_memory},#{@right_memory})(#{@not_memory}) -> #{@out_nodes}"
583
+ @left_input.print(tab + ' ')
584
+ @right_input.print(tab + ' ')
585
+ end
586
+
587
+ # This method filters the match results that are given by the left and
588
+ # right patterns. It does this by merging the two. It needs to be
589
+ # refactored, and probably moved into the Pattern that this JoinNode wraps
590
+ # (so we can take a different approach for OrPatterns in the future).
591
+ def filter_match_results(left, right)
592
+ mc = []
593
+ left.each do |left_mr|
594
+ right.each do |right_mr|
595
+ if right_mr.keys.empty?
596
+ # HACK we're doing this because we have an empty MatchResult
597
+ # from a NotNode.
598
+ # QUESTION do we need to account for these on the left too?
599
+ mc.push left_mr.dup unless matches_not?(left_mr,@not_memory.match_results)
600
+ else
601
+ if resolve left_mr, right_mr
602
+ mr = left_mr.dup.update(right_mr.dup)
603
+ mr.fact_hash.update left_mr.fact_hash
604
+ mr.fact_hash.update right_mr.fact_hash
605
+ mc.push mr unless matches_not?(mr,@not_memory.match_results)
606
+ # TODO cache the mr's that fail the test above, and add them in
607
+ # if the fact that generated that not_match_result is retracted?
608
+ end
609
+ end
610
+ end
611
+ end
612
+ return mc
613
+ end
614
+ private:filter_match_results
615
+ end
616
+
617
+ # This class represents a final node in the network. It has no outputs. It
618
+ # is responsible for creating, and maintaining the list of activations for the
619
+ # node network.
620
+ class TerminalNode < Node
621
+
622
+ def initialize(rule)
623
+ super(nil)
624
+ @rule = rule
625
+ @activations = DoubleHash.new
626
+ @parent_node = nil #only used for printing the network
627
+ end
628
+
629
+ attr_reader :rule
630
+ attr :parent_node, true
631
+ attr_reader :activations
632
+
633
+ def assert(in_node,mr,fact)
634
+ if verify_by_tags(mr, @rule.pattern.atom_tags)
635
+ @activations.add mr.fact_ids, Activation.new(@rule.action, mr)
636
+ end
637
+ end
638
+
639
+ def retract(in_node,fact)
640
+ @activations.remove(fact.id)
641
+ end
642
+
643
+ def assert_neg(in_node,mr,fact)
644
+ @activations.delete_if { |a| matches_not?(a.match,[mr]) }
645
+ end
646
+
647
+ def retract_neg(in_node,fact)
648
+ # we don't need to do anything here, because the nodes above us will
649
+ # handle the assertion of new MatchResults; hence Activations
650
+ end
651
+
652
+ def print(tab)
653
+ puts tab + 'TerminalNode: ' + satisfied?.to_s + "(#{@activations.values})(#{@match_context})"
654
+ @parent_node.print(tab + ' ')
655
+ end
656
+
657
+ # HACK This method is used to ensure that all required atoms have a value
658
+ # present in this match result. If any are missing, then the match result
659
+ # is incomplete. Maybe this should be moved up to be done within the context
660
+ # of matching. Each MatchContext could keep a hash/list of atoms it needs
661
+ # to be satisfied, and check them off as they are.
662
+ def verify_by_tags(match, tags)
663
+ tags.each do |t|
664
+ if t.kind_of?(Array)
665
+ # for future use by the OrPattern
666
+ return false unless verify_by_tags(match, t)
667
+ else
668
+ if t.exists
669
+ unless (match.key? t.tag)
670
+ return false
671
+ end
672
+ end
673
+ end
674
+ end
675
+ return true
676
+ end
677
+ private:verify_by_tags
678
+
679
+ def satisfied?
680
+ return !@activations.values.empty?
681
+ end
682
+ end
683
+ end
684
+ end