ruleby 0.2 → 0.3

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.
@@ -1,3 +1,14 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner
10
+ #
11
+
1
12
  module Ruleby
2
13
  module Core
3
14
 
@@ -7,10 +18,7 @@ module Ruleby
7
18
  class RootNode
8
19
  def initialize(working_memory)
9
20
  @working_memory = working_memory
10
-
11
- # TODO once node sharing is implemented, @type_nodes will be a
12
- # Class=>TypeNode Hash.
13
- @type_nodes = []
21
+ @type_nodes = {}
14
22
  @atom_nodes = []
15
23
  @join_nodes = []
16
24
  @terminal_nodes = []
@@ -20,171 +28,51 @@ module Ruleby
20
28
  # rule is processed and the appropriate nodes are added to the network.
21
29
  def assert_rule(rule)
22
30
  pattern = rule.pattern
23
- terminal_node = TerminalNode.new rule
31
+ terminal_node = TerminalNode.new rule
24
32
  build_network(pattern, terminal_node)
25
33
  @terminal_nodes.push terminal_node
26
- end
27
-
28
- # This method builds the network by starting at the bottom and recursively
29
- # working its way to the top. The recursion goes up the left side of the
30
- # tree first (depth first... but our tree is up-side-down).
31
- # pattern - the pattern to process (Single or Composite)
32
- # out_node - the node that will be below the new node in the network
33
- # side - if the out_node is a JoinNode, this marks the side of the new node
34
- # Returns a new node in the network that wraps the given pattern and
35
- # is above (i.e. it outputs to) the given node.
36
- def build_network(pattern, out_node, side=nil)
37
- if (pattern.kind_of?(ObjectPattern))
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
41
- else
42
- join_node = create_join_node(pattern, out_node, side)
43
- build_network(pattern.left_pattern, join_node, :left)
44
- build_network(pattern.right_pattern, join_node, :right)
45
- out_node.parent_nodes.push join_node # only used to print network
46
- return join_node
47
- end
48
- end
49
- private:build_network
50
-
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])
59
-
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
76
-
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
86
- end
87
- private:create_atom_nodes
88
-
89
- # Creates a JoinNode, puts it at the middle of the network, and stores
90
- # the node below it into its memory.
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
102
- end
103
-
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
112
- return join_node
113
- end
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
152
-
34
+ end
35
+
153
36
  # When a new fact is added to working memory, or an existing one is removed
154
37
  # this method is called. It finds any nodes that depend on it, and updates
155
38
  # them accordingly.
156
39
  def assert_fact(fact)
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|
40
+ node = @type_nodes[fact.object.class]
41
+ if node
160
42
  if fact.token == :plus
161
- node.assert(MatchContext.new(fact))
43
+ node.assert(fact)
162
44
  else
163
45
  node.retract fact
164
46
  end
165
47
  end
166
48
  end
49
+
50
+ # Increments the activation counter. This is just a pass-thru to the static
51
+ # variable in the terminal node
52
+ def increment_counter
53
+ TerminalNode.increment_counter
54
+ end
167
55
 
168
- # This method is used to update each TypeAtomNode based on
169
- # the facts in working memory. It can be a costly operation because it
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)
175
- end
56
+ # Resets the activation counter. This is just a pass-thru to the static
57
+ # variable in the terminal node
58
+ def reset_counter
59
+ TerminalNode.reset_counter
176
60
  end
177
- private:compare_to_wm
178
61
 
179
62
  # When invoked, this method returns a list of all Action|MatchContext pairs
180
63
  # as Activations. The list is generated when facts and rules are asserted,
181
64
  # so no comparisions are done here (i.e. no Backward Chaining).
182
- def match
65
+ def matches(initial=true)
183
66
  agenda = Array.new
184
67
  @terminal_nodes.each do |node|
185
- if (node.satisfied?)
186
- agenda.concat node.activations.values
187
- end
68
+ node.activations.values.each do |a|
69
+ if initial
70
+ a.used = false
71
+ agenda.push a
72
+ elsif !a.used
73
+ agenda.push a
74
+ end
75
+ end
188
76
  end
189
77
  return agenda
190
78
  end
@@ -195,17 +83,165 @@ module Ruleby
195
83
  node.print(' ')
196
84
  end
197
85
  end
86
+
87
+ private
88
+
89
+ # This method builds the network by starting at the bottom and recursively
90
+ # working its way to the top. The recursion goes up the left side of the
91
+ # tree first (depth first... but our tree is up-side-down).
92
+ # pattern - the pattern to process (Single or Composite)
93
+ # out_node - the node that will be below the new node in the network
94
+ # side - if the out_node is a JoinNode, this marks the side
95
+ # Returns a new node in the network that wraps the given pattern and
96
+ # is above (i.e. it outputs to) the given node.
97
+ def build_network(pattern, out_node, side=nil)
98
+ if (pattern.kind_of?(ObjectPattern))
99
+ atom_node = create_atom_nodes(pattern, out_node, side)
100
+ #out_node.parent_nodes.push atom_node # only used to print network
101
+ return atom_node
102
+ else
103
+ join_node = create_join_node(pattern, out_node, side)
104
+ build_network(pattern.left_pattern, join_node, :left)
105
+ build_network(pattern.right_pattern, join_node, :right)
106
+ out_node.parent_nodes.push join_node # only used to print network
107
+ return join_node
108
+ end
109
+ end
110
+
111
+ # This method is used to create the atom nodes that make up the given
112
+ # pattern's network. It returns the node that is at the top of the
113
+ # network for the pattern.
114
+ # pattern - the Pattern that the created node wraps
115
+ # out_node - the Node that this pattern is directly above in thw network
116
+ # side - if the out_node is a JoinNode, this marks the side
117
+ def create_atom_nodes(pattern, out_node, side)
118
+ # TODO refactor this method so it clear and concise
119
+ type_node = create_type_node(pattern)
120
+
121
+ parent_node = type_node
122
+ for i in (1..(pattern.atoms.size-1))
123
+ node = nil
124
+ atom = pattern.atoms[i]
125
+ if atom.kind_of?(SelfReferenceAtom)
126
+ node = create_self_reference_node(atom)
127
+ elsif atom.kind_of?(ReferenceAtom)
128
+ node = create_reference_node(atom)
129
+ out_node.ref_nodes.push node
130
+ else
131
+ node = create_property_node(atom)
132
+ end
133
+ parent_node.out_nodes.push node
134
+ node.parent_nodes.push parent_node
135
+ parent_node = node
136
+ end
137
+
138
+ bridge_node = create_bridge_node(pattern)
139
+ parent_node.out_nodes.push bridge_node
140
+ bridge_node.parent_nodes.push parent_node
141
+ parent_node = bridge_node
142
+
143
+ out_node.parent_nodes.push parent_node
144
+
145
+ if out_node.kind_of?(JoinNode)
146
+ adapter_node = create_adapter_node(side)
147
+ parent_node.out_nodes.push adapter_node
148
+ parent_node = adapter_node
149
+ end
150
+
151
+ parent_node.out_nodes.push out_node
152
+ compare_to_wm(type_node)
153
+ return type_node
154
+ end
155
+
156
+ # Creates a JoinNode, puts it at the middle of the network, and stores
157
+ # the node below it into its memory.
158
+ # pattern - the Pattern that the created node wraps
159
+ # out_node - the Node that this pattern is directly above in thw network
160
+ # side - if the out_node is a JoinNode, this marks the side
161
+ def create_join_node(pattern, out_node, side)
162
+ join_node = nil
163
+ if (pattern.left_pattern.kind_of?(NotPattern))
164
+ raise 'NotPatterns at the being of a rule are not yet supported'
165
+ elsif (pattern.right_pattern.kind_of?(NotPattern))
166
+ join_node = NotNode.new
167
+ else
168
+ join_node = JoinNode.new
169
+ end
170
+
171
+ @join_nodes.push(join_node)
172
+ parent_node = join_node
173
+ if out_node.kind_of?(JoinNode)
174
+ adapter_node = create_adapter_node(side)
175
+ parent_node.out_nodes.push adapter_node
176
+ parent_node = adapter_node
177
+ end
178
+ parent_node.out_nodes.push out_node
179
+ return join_node
180
+ end
181
+
182
+ def create_type_node(pattern)
183
+ atom = pattern.atoms[0]
184
+ node = @type_nodes[atom.clazz]
185
+ unless node
186
+ node = TypeNode.new atom
187
+ @type_nodes[atom.clazz] = node
188
+ end
189
+ return node
190
+ end
191
+
192
+ def create_bridge_node(pattern)
193
+ return BridgeNode.new(pattern)
194
+ end
195
+
196
+ def create_property_node(atom)
197
+ node = PropertyNode.new atom
198
+ @atom_nodes.push node
199
+ return node
200
+ end
201
+
202
+ def create_self_reference_node(atom)
203
+ node = SelfReferenceNode.new atom
204
+ @atom_nodes.push node
205
+ return node
206
+ end
207
+
208
+ def create_reference_node(atom)
209
+ node = ReferenceNode.new atom
210
+ @atom_nodes.push node
211
+ return node
212
+ end
213
+
214
+ def create_adapter_node(side)
215
+ if side == :left
216
+ return LeftAdapterNode.new
217
+ else
218
+ return RightAdapterNode.new
219
+ end
220
+ end
221
+
222
+ # This method is used to update each TypeNode based on the facts in
223
+ # working memory. It can be a costly operation because it iterates over
224
+ # EVERY fact in working memory. It should only be used when a new rule is
225
+ # added.
226
+ def compare_to_wm(type_node)
227
+ @working_memory.each_fact do |fact|
228
+ type_node.assert MatchContext.new(fact)
229
+ end
230
+ end
198
231
  end
199
232
 
200
233
  # 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).
234
+ # provides handles to the nodes above it in the network. These are not used
235
+ # for matching (i.e. no backward-chaining).
203
236
  class Printable
237
+
238
+ attr_reader:parent_nodes
239
+
204
240
  def initialize
205
- # this only used for printing the network, not for matching
241
+ # this is only used for printing the network, not for matching
206
242
  @parent_nodes = []
207
243
  end
208
- attr_reader:parent_nodes
244
+
209
245
  def print(tab)
210
246
  puts tab + to_s
211
247
  @parent_nodes.each do |out_node|
@@ -216,7 +252,7 @@ module Ruleby
216
252
 
217
253
  # Base Node class used by all nodes in the network that do some kind
218
254
  # of matching.
219
- class Node < Printable
255
+ class Node < Printable
220
256
 
221
257
  # This method determines if all common tags have equal values. If any
222
258
  # values are not equal then the method returns false.
@@ -236,96 +272,61 @@ module Ruleby
236
272
  # other node (i.e. they are not at the bottom). It contains methods for
237
273
  # propagating match results.
238
274
  class ParentNode < Node
239
- def initialize
240
- super
241
- @out_nodes = []
242
- end
243
- attr_reader:out_nodes
275
+
276
+ attr_reader:out_nodes
244
277
 
245
- def assert(context)
246
- propagate_assert(context)
247
- end
278
+ def initialize()
279
+ super
280
+ @out_nodes = []
281
+ end
248
282
 
249
283
  def retract(fact)
250
284
  propagate_retract(fact)
251
- end
285
+ end
252
286
 
253
- def propagate_assert(context)
287
+ def propagate_retract(fact)
254
288
  @out_nodes.each do |out_node|
255
- out_node.assert(context)
289
+ out_node.retract(fact)
256
290
  end
257
291
  end
258
292
 
259
- def propagate_retract(fact)
293
+ def assert(assertable)
294
+ propagate_assert(assertable)
295
+ end
296
+
297
+ def propagate_assert(assertable)
260
298
  @out_nodes.each do |out_node|
261
- out_node.retract(fact)
299
+ out_node.assert(assertable)
262
300
  end
263
301
  end
264
302
  end
265
303
 
266
304
  # 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.
305
+ # some properties. It is essentially a wrapper for an Atom. These nodes make
306
+ # up the Alpha network.
268
307
  class AtomNode < ParentNode
308
+
309
+ attr_reader:atom
310
+
269
311
  def initialize(atom)
270
312
  super()
271
313
  @atom = atom
272
314
  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
315
  end
281
316
 
282
317
  # 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)
289
- end
318
+ class TypeNode < AtomNode
319
+ def assert(fact)
320
+ propagate_assert(fact) if (@atom.clazz == fact.object.class)
290
321
  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
322
  end
305
323
 
306
324
  # This node class is used for matching properties of a fact.
307
325
  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)
314
- end
315
- end
316
-
317
- def match(fact)
318
- mr = MatchResult.new
326
+ def assert(fact)
319
327
  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
328
+ propagate_assert(fact) if @atom.proc.call(val)
327
329
  end
328
- private:match
329
330
  end
330
331
 
331
332
  # This node class is used to match properties of one with the properties
@@ -336,8 +337,7 @@ module Ruleby
336
337
  def match(left_context,right_fact)
337
338
  val = right_fact.object.send("#{@atom.name}")
338
339
  args = [val]
339
- # TODO for node sharing we will have to compare against multiple matches
340
- match = left_context.match.dup
340
+ match = left_context.match
341
341
  @atom.vars.each do |var|
342
342
  args.push match.variables[var]
343
343
  end
@@ -354,16 +354,51 @@ module Ruleby
354
354
 
355
355
  # This node class is used to match properties of a fact with other properties
356
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)
363
- end
357
+ class SelfReferenceNode < AtomNode
358
+ def assert(fact)
359
+ propagate_assert fact if match fact
360
+ end
361
+
362
+ def match(fact)
363
+ args = [fact.object.send("#{@atom.name}")]
364
+ @atom.vars.each do |var|
365
+ args.push fact.object.send(var)
366
+ end
367
+ return @atom.proc.call(*args)
364
368
  end
365
369
  end
366
370
 
371
+ # The BridgeNode is used to bridge the alpha network to either the beta
372
+ # network, or to the terminal nodes. It creates a partial match from the
373
+ # pattern and atoms above it in the network. Thus, there is one bridge node
374
+ # for each pattern (assuming they aren't shared).
375
+ class BridgeNode < ParentNode
376
+ def initialize(pattern)
377
+ super()
378
+ @pattern = pattern
379
+ end
380
+
381
+ def propagate_assert(fact)
382
+ # create the partial match
383
+ mr = MatchResult.new
384
+ mr.is_match = true
385
+ mr.recency.push fact.recency
386
+ @pattern.atoms.each do |atom|
387
+ mr.fact_hash[atom.tag] = fact.id
388
+ if atom == @pattern.head
389
+ # HACK its a pain to have to check for this, can we make it special
390
+ mr[atom.tag] = fact.object
391
+ else
392
+ mr[atom.tag] = fact.object.send("#{atom.name}")
393
+ end
394
+ end
395
+
396
+ context = MatchContext.new(fact,mr)
397
+
398
+ super(context)
399
+ end
400
+ end
401
+
367
402
  # This class is used to plug nodes into the left input of a two-input JoinNode
368
403
  class LeftAdapterNode < ParentNode
369
404
  def propagate_assert(context)
@@ -379,8 +414,9 @@ module Ruleby
379
414
  end
380
415
  end
381
416
 
382
- # This class is used to plug nodes into the right input of a two-input JoinNode
383
- class RightAdapterNode < ParentNode
417
+ # This class is used to plug nodes into the right input of a two-input
418
+ # JoinNode
419
+ class RightAdapterNode < ParentNode
384
420
  def propagate_assert(context)
385
421
  @out_nodes.each do |out_node|
386
422
  out_node.assert_right(context)
@@ -396,15 +432,18 @@ module Ruleby
396
432
 
397
433
  # This class is a two-input node that is used to create a cross-product of the
398
434
  # two network branches above it. It keeps a memory of the left and right
399
- # inputs and compares new facts to each.
435
+ # inputs and compares new facts to each. These nodes make up what is called
436
+ # the Beta network.
400
437
  class JoinNode < ParentNode
438
+
439
+ attr:ref_nodes,true
440
+
401
441
  def initialize
402
442
  super
403
443
  @left_memory = {}
404
444
  @right_memory = {}
405
445
  @ref_nodes = []
406
446
  end
407
- attr:ref_nodes,true
408
447
 
409
448
  def retract_left(fact)
410
449
  @left_memory.delete(fact.id)
@@ -421,7 +460,6 @@ module Ruleby
421
460
  @right_memory.values.each do |right_context|
422
461
  mr = match_ref_nodes(context,right_context)
423
462
  if (mr.is_match)
424
- # TODO for node sharing we will have to update multiple matches
425
463
  new_context = MatchContext.new context.fact, mr
426
464
  propagate_assert(new_context)
427
465
  end
@@ -433,62 +471,61 @@ module Ruleby
433
471
  @left_memory.values.flatten.each do |left_context|
434
472
  mr = match_ref_nodes(left_context,context)
435
473
  if (mr.is_match)
436
- # TODO for node sharing we will have to update multiple matches
437
- new_context = MatchContext.new context.fact, mr
474
+ new_context = MatchContext.new context.fact, mr
438
475
  propagate_assert(new_context)
439
476
  end
440
477
  end
478
+ end
479
+
480
+ def to_s
481
+ return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
441
482
  end
442
483
 
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
484
+ private
485
+ def match_ref_nodes(left_context,right_context)
486
+ mr = right_context.match
487
+ if @ref_nodes.empty?
488
+ return left_context.match.merge(mr)
489
+ else
490
+ @ref_nodes.each do |ref_node|
491
+ ref_mr = ref_node.match(left_context, right_context.fact)
492
+ if ref_mr.is_match
493
+ mr = mr.merge ref_mr
494
+ else
495
+ return MatchResult.new
496
+ end
455
497
  end
498
+ return mr
456
499
  end
457
- return mr
458
500
  end
459
- end
460
- private:match_ref_nodes
461
-
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
470
-
471
- def propagate_retract_resolve(match)
472
- @out_nodes.each do |o|
473
- o.retract_resolve(match)
501
+
502
+ def add_to_left_memory(context)
503
+ lm = @left_memory[context.fact.id]
504
+ lm = [] unless lm
505
+ lm.push context
506
+ @left_memory[context.fact.id] = lm
507
+ # QUESTION for a little while we were having trouble with duplicate
508
+ # contexts being added to the left_memory. Double check that this is
509
+ # not happening
474
510
  end
475
- end
476
-
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
511
+
512
+ def propagate_retract_resolve(match)
513
+ @out_nodes.each do |o|
514
+ o.retract_resolve(match)
515
+ end
516
+ end
517
+
518
+ def retract_resolve(match)
519
+ # in this method we retract an existing match from memory if it resolves
520
+ # with the match given. It would probably be better to check if it
521
+ # resolves with a list of facts. But the system is not set up for
522
+ # that yet.
523
+ @left_memory.each do |fact_id,contexts|
524
+ value.delete_if do |left_context|
525
+ resolve(left_context.match, match)
526
+ end
527
+ end
486
528
  end
487
- end
488
-
489
- def to_s
490
- return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
491
- end
492
529
  end
493
530
 
494
531
  # This node class is used when a rule is looking for a fact that does not
@@ -509,13 +546,15 @@ module Ruleby
509
546
  right_context = @right_memory.delete(fact.id)
510
547
  unless right_context == @right_memory.default
511
548
  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)
549
+ @left_memory.values.each do |lm|
550
+ lm.each do |left_context|
551
+ # TODO we should cache the matches on the left that were unmatched
552
+ # by a result from a NotPattern. We could hash them by the right
553
+ # match that caused this. That we way we would not have to
554
+ # re-compare the the left and right matches.
555
+ if match_ref_nodes(left_context,right_context)
556
+ propagate_assert(left_context)
557
+ end
519
558
  end
520
559
  end
521
560
  end
@@ -562,10 +601,12 @@ module Ruleby
562
601
  end
563
602
 
564
603
  # 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
604
+ # relation between TerminalNodes and Rules. A terminal node acts as a wrapper
566
605
  # for a rule. The class is responsible for keeping a memory of the
567
606
  # activations that have been generated by the network.
568
607
  class TerminalNode < Node
608
+ @@counter = 0
609
+
569
610
  def initialize(rule)
570
611
  super()
571
612
  @rule = rule
@@ -574,7 +615,9 @@ module Ruleby
574
615
  attr_reader:activations
575
616
 
576
617
  def assert(context)
577
- @activations.add context.match.fact_ids, Activation.new(@rule.action, context.match)
618
+ match = context.match
619
+ a = Activation.new(@rule.action, match, @@counter)
620
+ @activations.add match.fact_ids, a
578
621
  end
579
622
 
580
623
  def retract(fact)
@@ -598,24 +641,13 @@ module Ruleby
598
641
  resolve(activation.match, match)
599
642
  end
600
643
  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
644
 
613
- def to_s
614
- return @match.to_s
645
+ def self.increment_counter
646
+ @@counter = @@counter + 1
615
647
  end
616
648
 
617
- def ==(t)
618
- return @fact == t.fact && @match == t.match
649
+ def self.reset_counter
650
+ @@counter = 0
619
651
  end
620
652
  end
621
653