ruleby 0.2 → 0.3

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