ruleby 0.1 → 0.2

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.
@@ -73,6 +73,17 @@ module Ruleby
73
73
  attr_accessor:pattern
74
74
  attr_reader:action, :name
75
75
 
76
+
77
+ def when(&block)
78
+ wb = WhenBuilder.new
79
+ yield wb
80
+ @pattern = wb.pattern
81
+ end
82
+
83
+ def then(&block)
84
+ @action = Core::Action.new(&block)
85
+ end
86
+
76
87
  def when=(pattern)
77
88
  @pattern = pattern
78
89
  end
@@ -128,7 +139,11 @@ module Ruleby
128
139
  @facts = Array.new
129
140
  end
130
141
 
131
- attr_reader :facts
142
+ def each_fact
143
+ @facts.each do |f|
144
+ yield(f)
145
+ end
146
+ end
132
147
 
133
148
  def assert_fact(fact)
134
149
  raise 'The fact asserted cannot be nil!' unless fact.object
@@ -156,12 +171,27 @@ module Ruleby
156
171
  end
157
172
 
158
173
  class Engine
159
- def initialize()
174
+ def initialize(wm=WorkingMemory.new)
160
175
  @root = nil
161
- @working_memory = WorkingMemory.new
176
+ @working_memory = wm
162
177
  @conflict_resolver = RulebyConflictResolver.new
163
178
  end
164
-
179
+ #assert fact
180
+ def assert(object,&block)
181
+ fact_helper(object,:plus,&block)
182
+ end
183
+ #retract fact
184
+ def retract(object,&block)
185
+ fact_helper(object,:minus,&block)
186
+ end
187
+ #modify fact
188
+ # retract
189
+ # assert
190
+ def modify(object,&block)
191
+ retract(object,&block)
192
+ assert(object,&block)
193
+ end
194
+
165
195
  def assert_fact(fact)
166
196
  wm_fact = @working_memory.assert_fact fact
167
197
  @root.assert_fact wm_fact if @root != nil
@@ -173,8 +203,8 @@ module Ruleby
173
203
  end
174
204
 
175
205
  def match(agenda=@root.match, used_agenda=[], activation_counter=0)
176
- agenda = @conflict_resolver.resolve agenda
177
- if(agenda && agenda.length > 0)
206
+ while (agenda.length > 0)
207
+ agenda = @conflict_resolver.resolve agenda
178
208
  activation = agenda.pop
179
209
  used_agenda.push activation
180
210
  activation.fire self
@@ -197,15 +227,23 @@ module Ruleby
197
227
  end
198
228
  end
199
229
  agenda.delete_if {|a| new_agenda.index(a) == nil}
200
-
201
- match(agenda, used_agenda, activation_counter+1)
230
+
231
+ activation_counter = activation_counter+1
202
232
  end
203
233
  end
204
234
 
205
235
  def print
206
236
  @working_memory.print
207
237
  @root.print
208
- end
238
+ end
239
+
240
+ private
241
+ def fact_helper(object, sign=:plus, &block)
242
+ f = Core::Fact.new object, sign
243
+ yield f if block_given?
244
+ assert_fact f
245
+ f
246
+ end
209
247
  end
210
248
  end
211
249
  end
@@ -1,100 +1,19 @@
1
1
  module Ruleby
2
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
3
 
83
4
  # This class acts as the root-node of the network. It contains the logic
84
5
  # for building the node-network from a set of rules, and updating it
85
6
  # according to the working memory
86
- class RootNode
87
-
7
+ class RootNode
88
8
  def initialize(working_memory)
89
9
  @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
10
+
11
+ # TODO once node sharing is implemented, @type_nodes will be a
12
+ # Class=>TypeNode Hash.
13
+ @type_nodes = []
14
+ @atom_nodes = []
15
+ @join_nodes = []
16
+ @terminal_nodes = []
98
17
  end
99
18
 
100
19
  # This method is invoked when a new rule is added to the system. The
@@ -102,8 +21,7 @@ module Ruleby
102
21
  def assert_rule(rule)
103
22
  pattern = rule.pattern
104
23
  terminal_node = TerminalNode.new rule
105
- node = build_network(pattern, terminal_node)
106
- terminal_node.parent_node = node # only used to print the network
24
+ build_network(pattern, terminal_node)
107
25
  @terminal_nodes.push terminal_node
108
26
  end
109
27
 
@@ -117,140 +35,147 @@ module Ruleby
117
35
  # is above (i.e. it outputs to) the given node.
118
36
  def build_network(pattern, out_node, side=nil)
119
37
  if (pattern.kind_of?(ObjectPattern))
120
- return create_object_node(pattern, out_node, side)
38
+ atom_node = create_atom_nodes(pattern, out_node, side)
39
+ #out_node.parent_nodes.push atom_node # only used to print network
40
+ return atom_node
121
41
  else
122
42
  join_node = create_join_node(pattern, out_node, side)
123
43
  build_network(pattern.left_pattern, join_node, :left)
124
44
  build_network(pattern.right_pattern, join_node, :right)
45
+ out_node.parent_nodes.push join_node # only used to print network
125
46
  return join_node
126
47
  end
127
48
  end
128
49
  private:build_network
129
50
 
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
51
+ # This method is used to create the atom nodes that make up the given
52
+ # pattern's network. It returns the node that is at the top of the network
53
+ # for the pattern.
54
+ # pattern - the Pattern that the created node wraps
55
+ # out_node - the Node that this pattern is directly above in thw network
56
+ # side - if the out_node is a JoinNode, this marks the side of the new node
57
+ def create_atom_nodes(pattern, out_node, side)
58
+ type_node = create_type_node(pattern.atoms[0])
143
59
 
144
- if side==:left
145
- out_node.left_input = obj_node
146
- elsif side==:right
147
- out_node.right_input = obj_node
148
- end
60
+ parent_node = type_node
61
+ for i in (1..(pattern.atoms.size-1))
62
+ node = nil
63
+ if pattern.atoms[i].kind_of?(SelfReferenceAtom)
64
+ node = create_self_reference_node(pattern.atoms[i])
65
+ elsif pattern.atoms[i].kind_of?(ReferenceAtom)
66
+ node = create_reference_node(pattern.atoms[i])
67
+ out_node.ref_nodes.push node
68
+ else
69
+ node = create_property_node(pattern.atoms[i])
70
+ end
71
+ parent_node.out_nodes.push node
72
+ node.parent_nodes.push parent_node
73
+ parent_node = node
74
+ end
75
+ out_node.parent_nodes.push parent_node
149
76
 
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
77
+ if out_node.kind_of?(JoinNode)
78
+ adapter_node = create_adapter_node(side)
79
+ parent_node.out_nodes.push adapter_node
80
+ parent_node = adapter_node
81
+ end
82
+
83
+ parent_node.out_nodes.push out_node
84
+ compare_to_wm(type_node)
85
+ return type_node
163
86
  end
164
- private:create_object_node
87
+ private:create_atom_nodes
165
88
 
166
89
  # Creates a JoinNode, puts it at the middle of the network, and stores
167
90
  # 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
91
+ # pattern - the Pattern that the created node wraps
92
+ # out_node - the Node that this pattern is directly above in thw network
93
+ # side - if the out_node is a JoinNode, this marks the side of the new node
94
+ def create_join_node(pattern, out_node, side)
95
+ join_node = nil
96
+ if (pattern.left_pattern.kind_of?(NotPattern))
97
+ raise 'NotPatterns at the being of a rule are not yet supported'
98
+ elsif (pattern.right_pattern.kind_of?(NotPattern))
99
+ join_node = NotNode.new
100
+ else
101
+ join_node = JoinNode.new
178
102
  end
179
103
 
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
104
+ @join_nodes.push(join_node)
105
+ parent_node = join_node
106
+ if out_node.kind_of?(JoinNode)
107
+ adapter_node = create_adapter_node(side)
108
+ parent_node.out_nodes.push adapter_node
109
+ parent_node = adapter_node
110
+ end
111
+ parent_node.out_nodes.push out_node
184
112
  return join_node
185
113
  end
186
114
  private:create_join_node
115
+
116
+ def create_type_node(atom)
117
+ node = TypeNode.new atom
118
+ @type_nodes.push node
119
+ return node
120
+ end
121
+ private:create_type_node
122
+
123
+ def create_property_node(atom)
124
+ node = PropertyNode.new atom
125
+ @atom_nodes.push node
126
+ return node
127
+ end
128
+ private:create_property_node
129
+
130
+ def create_self_reference_node(atom)
131
+ node = SelfReferenceNode.new atom
132
+ @atom_nodes.push node
133
+ return node
134
+ end
135
+ private:create_self_reference_node
136
+
137
+ def create_reference_node(atom)
138
+ node = ReferenceNode.new atom
139
+ @atom_nodes.push node
140
+ return node
141
+ end
142
+ private:create_reference_node
143
+
144
+ def create_adapter_node(side)
145
+ if side == :left
146
+ return LeftAdapterNode.new
147
+ else
148
+ return RightAdapterNode.new
149
+ end
150
+ end
151
+ private:create_adapter_node
187
152
 
188
153
  # When a new fact is added to working memory, or an existing one is removed
189
154
  # this method is called. It finds any nodes that depend on it, and updates
190
155
  # them accordingly.
191
156
  def assert_fact(fact)
192
- @object_nodes.each fact.object.class do |object_node|
193
- altered = false
157
+ # TODO implement node sharing. Instead of iterating over the @type_nodes
158
+ # list, we will just get the one that matches the type of this fact
159
+ @type_nodes.each do |node|
194
160
  if fact.token == :plus
195
- altered = object_node.assert fact
161
+ node.assert(MatchContext.new(fact))
196
162
  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)
163
+ node.retract fact
213
164
  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
165
+ end
231
166
  end
232
167
 
233
- # This method is used to update each ObjectNode in the given list based on
168
+ # This method is used to update each TypeAtomNode based on
234
169
  # 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
170
+ # iterates over EVERY fact in working memory. It should only be used when a
171
+ # new rule is added.
172
+ def compare_to_wm(type_node)
173
+ @working_memory.each_fact do |fact|
174
+ type_node.assert MatchContext.new(fact)
250
175
  end
251
- return altered
252
176
  end
253
-
177
+ private:compare_to_wm
178
+
254
179
  # When invoked, this method returns a list of all Action|MatchContext pairs
255
180
  # as Activations. The list is generated when facts and rules are asserted,
256
181
  # so no comparisions are done here (i.e. no Backward Chaining).
@@ -258,7 +183,7 @@ module Ruleby
258
183
  agenda = Array.new
259
184
  @terminal_nodes.each do |node|
260
185
  if (node.satisfied?)
261
- agenda.concat node.activations.values.values
186
+ agenda.concat node.activations.values
262
187
  end
263
188
  end
264
189
  return agenda
@@ -267,418 +192,432 @@ module Ruleby
267
192
  def print
268
193
  puts 'NETWORK:'
269
194
  @terminal_nodes.each do |node|
270
- node.print(' ')
195
+ node.print(' ')
196
+ end
197
+ end
198
+ end
199
+
200
+ # Any node in the network that needs to be printed extends this class. It
201
+ # provides handles to the nodes above it in network. These are not used for
202
+ # matching (i.e. no backward-chaining).
203
+ class Printable
204
+ def initialize
205
+ # this only used for printing the network, not for matching
206
+ @parent_nodes = []
207
+ end
208
+ attr_reader:parent_nodes
209
+ def print(tab)
210
+ puts tab + to_s
211
+ @parent_nodes.each do |out_node|
212
+ out_node.print(' '+tab)
271
213
  end
272
214
  end
273
215
  end
274
216
 
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)
217
+ # Base Node class used by all nodes in the network that do some kind
218
+ # of matching.
219
+ class Node < Printable
220
+
221
+ # This method determines if all common tags have equal values. If any
222
+ # values are not equal then the method returns false.
223
+ def resolve(mr1, mr2)
224
+ mr1.variables.each_key do |t1|
225
+ mr2.variables.each_key do |t2|
226
+ if t2 == t1 && mr1.fact_hash[t1] != mr2.fact_hash[t2]
227
+ return false
228
+ end
229
+ end
230
+ end
231
+ return true
232
+ end
233
+ end
234
+
235
+ # This is the base class for all nodes in the network that output to some
236
+ # other node (i.e. they are not at the bottom). It contains methods for
237
+ # propagating match results.
238
+ class ParentNode < Node
239
+ def initialize
279
240
  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
241
+ @out_nodes = []
242
+ end
243
+ attr_reader:out_nodes
298
244
 
299
- def satisfied?
300
- return !@obj_results_hash.empty?
245
+ def assert(context)
246
+ propagate_assert(context)
301
247
  end
302
248
 
303
- def obj_results
304
- return @obj_results_hash.values
249
+ def retract(fact)
250
+ propagate_retract(fact)
305
251
  end
306
252
 
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
253
+ def propagate_assert(context)
254
+ @out_nodes.each do |out_node|
255
+ out_node.assert(context)
311
256
  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
257
+ end
258
+
259
+ def propagate_retract(fact)
260
+ @out_nodes.each do |out_node|
261
+ out_node.retract(fact)
345
262
  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
263
+ end
264
+ end
265
+
266
+ # This is a base class for all single input nodes that match facts based on
267
+ # some properties. It is essentially a wrapper for an Atom.
268
+ class AtomNode < ParentNode
269
+ def initialize(atom)
270
+ super()
271
+ @atom = atom
272
+ end
273
+ attr_reader:atom
274
+
275
+ def retract(fact)
276
+ # These nodes do not have a memory, so there is no need to do anything
277
+ # when a fact is retracted. Just pass it on to the two-input nodes.
278
+ propagate_retract(fact)
279
+ end
280
+ end
281
+
282
+ # This node class is used to match the type of a fact.
283
+ class TypeNode < AtomNode
284
+ def assert(context)
285
+ mr = match(context.fact)
286
+ if mr.is_match
287
+ context.match.update mr
288
+ propagate_assert(context)
375
289
  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
290
+ end
291
+
292
+ def match(fact)
293
+ mr = MatchResult.new
294
+ val = fact.object.send("#{@atom.name}")
295
+ if @atom.proc.call(val)
296
+ mr.is_match = true
297
+ mr.recency.push fact.recency
298
+ mr.fact_hash[@atom.tag] = fact.id
299
+ mr[@atom.tag] = fact.object
300
+ end
301
+ return mr
302
+ end
303
+ private:match
304
+ end
305
+
306
+ # This node class is used for matching properties of a fact.
307
+ class PropertyNode < AtomNode
308
+ def assert(context)
309
+ mr = match(context.fact)
310
+ if mr.is_match
311
+ # TODO for node sharing will we have to update multiple matches
312
+ context.match.update mr
313
+ propagate_assert(context)
402
314
  end
403
315
  end
404
316
 
405
- def print(tab)
406
- puts tab + to_s + " (#{satisfied?}) #{@pattern} -> #{@out_nodes}"
317
+ def match(fact)
318
+ mr = MatchResult.new
319
+ val = fact.object.send("#{@atom.name}")
320
+ if @atom.proc.call(val)
321
+ mr.is_match = true
322
+ mr.fact_hash[@atom.tag] = fact.id
323
+ mr.recency.push fact.recency
324
+ mr[@atom.tag] = val
325
+ end
326
+ return mr
407
327
  end
328
+ private:match
408
329
  end
409
330
 
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...
331
+ # This node class is used to match properties of one with the properties
332
+ # of any other already matched fact. It differs from the other AtomNodes
333
+ # because it does not perform any inline matching. The match method is only
334
+ # invoked by the two input node.
335
+ class ReferenceNode < AtomNode
336
+ def match(left_context,right_fact)
337
+ val = right_fact.object.send("#{@atom.name}")
338
+ args = [val]
339
+ # TODO for node sharing we will have to compare against multiple matches
340
+ match = left_context.match.dup
341
+ @atom.vars.each do |var|
342
+ args.push match.variables[var]
343
+ end
344
+ if @atom.proc.call(*args)
345
+ m = MatchResult.new(match.variables.clone, true, match.fact_hash.clone, match.recency)
346
+ m.recency.push right_fact.recency
347
+ m.fact_hash[@atom.tag] = right_fact.id
348
+ m.variables[@atom.tag] = val
349
+ return m
350
+ end
351
+ return MatchResult.new
418
352
  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
353
+ end
354
+
355
+ # This node class is used to match properties of a fact with other properties
356
+ # of itself. Unlike ReferenceAtom it does perform inline matching.
357
+ class SelfReferenceNode < ReferenceNode
358
+ def assert(context)
359
+ mr = match(context, context.fact)
360
+ if mr.is_match
361
+ context.match.update mr
362
+ propagate_assert(context)
438
363
  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
364
+ end
365
+ end
366
+
367
+ # This class is used to plug nodes into the left input of a two-input JoinNode
368
+ class LeftAdapterNode < ParentNode
369
+ def propagate_assert(context)
370
+ @out_nodes.each do |out_node|
371
+ out_node.assert_left(context)
475
372
  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
373
  end
484
374
 
485
- def print(tab)
486
- puts tab + to_s + " (#{satisfied?}) #{@pattern} -> #{@out_nodes}"
375
+ def propagate_retract(fact)
376
+ @out_nodes.each do |out_node|
377
+ out_node.retract_left(fact)
378
+ end
487
379
  end
488
380
  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
381
 
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
382
+ # This class is used to plug nodes into the right input of a two-input JoinNode
383
+ class RightAdapterNode < ParentNode
384
+ def propagate_assert(context)
385
+ @out_nodes.each do |out_node|
386
+ out_node.assert_right(context)
545
387
  end
546
- @local_memory.remove fact.id
547
- propagate_retract fact
548
388
  end
389
+
390
+ def propagate_retract(fact)
391
+ @out_nodes.each do |out_node|
392
+ out_node.retract_right(fact)
393
+ end
394
+ end
395
+ end
396
+
397
+ # This class is a two-input node that is used to create a cross-product of the
398
+ # two network branches above it. It keeps a memory of the left and right
399
+ # inputs and compares new facts to each.
400
+ class JoinNode < ParentNode
401
+ def initialize
402
+ super
403
+ @left_memory = {}
404
+ @right_memory = {}
405
+ @ref_nodes = []
406
+ end
407
+ attr:ref_nodes,true
549
408
 
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])
409
+ def retract_left(fact)
410
+ @left_memory.delete(fact.id)
411
+ propagate_retract(fact)
412
+ end
413
+
414
+ def retract_right(fact)
415
+ @right_memory.delete(fact.id)
416
+ propagate_retract(fact)
417
+ end
418
+
419
+ def assert_left(context)
420
+ add_to_left_memory(context)
421
+ @right_memory.values.each do |right_context|
422
+ mr = match_ref_nodes(context,right_context)
423
+ if (mr.is_match)
424
+ # TODO for node sharing we will have to update multiple matches
425
+ new_context = MatchContext.new context.fact, mr
426
+ propagate_assert(new_context)
427
+ end
555
428
  end
556
- propagate_assert_neg nmr, fact
557
429
  end
558
430
 
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
431
+ def assert_right(context)
432
+ @right_memory[context.fact.id] = context
433
+ @left_memory.values.flatten.each do |left_context|
434
+ mr = match_ref_nodes(left_context,context)
435
+ if (mr.is_match)
436
+ # TODO for node sharing we will have to update multiple matches
437
+ new_context = MatchContext.new context.fact, mr
438
+ propagate_assert(new_context)
439
+ end
578
440
  end
579
441
  end
580
442
 
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
443
+ def match_ref_nodes(left_context,right_context)
444
+ mr = right_context.match.dup
445
+ if @ref_nodes.empty?
446
+ return left_context.match.dup.update(mr)
447
+ else
448
+ # TODO for node sharing we will have to have a list of matches
449
+ @ref_nodes.each do |ref_node|
450
+ ref_mr = ref_node.match(left_context, right_context.fact)
451
+ if ref_mr.is_match
452
+ mr.update ref_mr
453
+ else
454
+ return MatchResult.new
609
455
  end
610
456
  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
457
+ return mr
458
+ end
627
459
  end
460
+ private:match_ref_nodes
628
461
 
629
- attr_reader :rule
630
- attr :parent_node, true
631
- attr_reader :activations
462
+ def add_to_left_memory(context)
463
+ lm = @left_memory[context.fact.id]
464
+ lm = [] unless lm
465
+ lm.push context
466
+ @left_memory[context.fact.id] = lm
467
+ # QUESTION for a little while we were having trouble with duplicate contexts
468
+ # being added to the left_memory. Double check that this is not happening
469
+ end
632
470
 
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)
471
+ def propagate_retract_resolve(match)
472
+ @out_nodes.each do |o|
473
+ o.retract_resolve(match)
636
474
  end
637
475
  end
638
476
 
639
- def retract(in_node,fact)
640
- @activations.remove(fact.id)
477
+ def retract_resolve(match)
478
+ # in this method we retract an existing match from memory if it resolves
479
+ # with the match given. It would probably be better to check if it
480
+ # resolves with a list of facts. But the system is not set up for
481
+ # that yet.
482
+ @left_memory.each do |fact_id,contexts|
483
+ value.delete_if do |left_context|
484
+ resolve(left_context.match, match)
485
+ end
486
+ end
641
487
  end
642
488
 
643
- def assert_neg(in_node,mr,fact)
644
- @activations.delete_if { |a| matches_not?(a.match,[mr]) }
489
+ def to_s
490
+ return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
491
+ end
492
+ end
493
+
494
+ # This node class is used when a rule is looking for a fact that does not
495
+ # exist. It is a two-input node, and thus has some of the properties of the
496
+ # JoinNode. NOTE it has not clear how this will work if the NotPattern is
497
+ # declared as the first pattern in a rule.
498
+ class NotNode < JoinNode
499
+ def initialize
500
+ super
645
501
  end
646
502
 
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
503
+ def retract_left(fact)
504
+ @left_memory.delete(fact.id)
505
+ propagate_retract(fact)
650
506
  end
651
507
 
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
508
+ def retract_right(fact)
509
+ right_context = @right_memory.delete(fact.id)
510
+ unless right_context == @right_memory.default
511
+ unless @ref_nodes.empty? && !@right_memory.empty?
512
+ @left_memory.values.each do |left_context|
513
+ # TODO we should cache the matches on the left that were unmatched
514
+ # by a result from a NotPattern. We could hash them by the right
515
+ # match that caused this. In that we woould not have to re-compare
516
+ # the the left and right matches.
517
+ if match_ref_nodes(left_context,right_context)
518
+ propagate_assert(left_context)
671
519
  end
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ def assert_left(context)
526
+ add_to_left_memory(context)
527
+ unless @ref_nodes.empty? && @right_memory.empty?
528
+ propagate = true
529
+ @right_memory.values.each do |right_context|
530
+ if match_ref_nodes(context,right_context)
531
+ propagate = false
532
+ break
533
+ end
534
+ end
535
+ propagate_assert(context) if propagate
536
+ end
537
+ end
538
+
539
+ def assert_right(context)
540
+ @right_memory[context.fact.id] = context
541
+ unless @ref_nodes.empty?
542
+ @left_memory.values.flatten.each do |left_context|
543
+ if match_ref_nodes(left_context,context)
544
+ # QUESTION is there a more efficient way to retract here?
545
+ propagate_retract_resolve(left_context.match)
672
546
  end
673
- end
547
+ end
548
+ end
549
+ end
550
+
551
+ # NOTE this returns a boolean, while the other classes return a MatchResult
552
+ def match_ref_nodes(left_context,right_context)
553
+ @ref_nodes.each do |ref_node|
554
+ ref_mr = ref_node.match(left_context, right_context.fact)
555
+ unless ref_mr.is_match
556
+ return false
557
+ end
674
558
  end
675
559
  return true
676
560
  end
677
- private:verify_by_tags
561
+ private:match_ref_nodes
562
+ end
563
+
564
+ # This class represents the bottom node in the network. There is a one to one
565
+ # relation betweek TerminalNodes and Rules. A terminal node acts as a wrapper
566
+ # for a rule. The class is responsible for keeping a memory of the
567
+ # activations that have been generated by the network.
568
+ class TerminalNode < Node
569
+ def initialize(rule)
570
+ super()
571
+ @rule = rule
572
+ @activations = MultiHash.new
573
+ end
574
+ attr_reader:activations
575
+
576
+ def assert(context)
577
+ @activations.add context.match.fact_ids, Activation.new(@rule.action, context.match)
578
+ end
579
+
580
+ def retract(fact)
581
+ @activations.remove fact.id
582
+ end
678
583
 
679
584
  def satisfied?
680
585
  return !@activations.values.empty?
586
+ end
587
+
588
+ def to_s
589
+ return "TerminalNode:#{object_id} | #{@activations.values}"
590
+ end
591
+
592
+ def retract_resolve(match)
593
+ # in this method we retract an existing activation from memory if its
594
+ # match resolves with the match given. It would probably be better to
595
+ # check if it resolves with a list of facts. But the system is not set up
596
+ # for that yet.
597
+ @activations.delete_if do |activation|
598
+ resolve(activation.match, match)
599
+ end
681
600
  end
682
- end
683
- end
601
+ end
602
+
603
+ class MatchContext
604
+ def initialize(fact,mr=MatchResult.new)
605
+ @fact = fact
606
+
607
+ # TODO for node sharing this will be a pattern=>match Hash
608
+ @match = mr
609
+ end
610
+ attr_reader:fact
611
+ attr_reader:match
612
+
613
+ def to_s
614
+ return @match.to_s
615
+ end
616
+
617
+ def ==(t)
618
+ return @fact == t.fact && @match == t.match
619
+ end
620
+ end
621
+
622
+ end
684
623
  end