ruleby 0.1

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