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.
- data/benchmarks/50_joined_rules.rb +78 -0
- data/benchmarks/50_rules.rb +57 -0
- data/benchmarks/5_joined_rules.rb +78 -0
- data/benchmarks/5_rules.rb +57 -0
- data/benchmarks/miss_manners/data.rb +135 -0
- data/benchmarks/miss_manners/miss_manners.rb +23 -0
- data/benchmarks/miss_manners/model.rb +182 -0
- data/benchmarks/miss_manners/rules.rb +165 -0
- data/examples/example_diagnosis.rb +155 -0
- data/examples/example_hello.rb +48 -0
- data/examples/example_politician.rb +86 -0
- data/examples/example_ticket.rb +158 -0
- data/examples/fibonacci_example1.rb +33 -0
- data/examples/fibonacci_example2.rb +29 -0
- data/examples/fibonacci_rulebook.rb +137 -0
- data/examples/test_self_reference.rb +35 -0
- data/lib/core/atoms.rb +13 -71
- data/lib/core/engine.rb +47 -9
- data/lib/core/nodes.rb +487 -548
- data/lib/core/patterns.rb +0 -110
- data/lib/core/utils.rb +120 -184
- data/lib/rulebook.rb +237 -157
- data/lib/ruleby.rb +0 -20
- metadata +28 -11
data/lib/core/engine.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
177
|
-
|
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
|
-
|
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
|
data/lib/core/nodes.rb
CHANGED
@@ -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
|
-
#
|
92
|
-
#
|
93
|
-
|
94
|
-
@
|
95
|
-
|
96
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
#
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
def
|
137
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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:
|
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
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
def create_join_node(pattern, out_node, side)
|
172
|
-
join_node =
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
193
|
-
|
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
|
-
|
161
|
+
node.assert(MatchContext.new(fact))
|
196
162
|
else
|
197
|
-
|
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
|
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
|
236
|
-
#
|
237
|
-
def
|
238
|
-
|
239
|
-
|
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
|
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
|
-
#
|
276
|
-
#
|
277
|
-
class
|
278
|
-
|
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
|
-
|
282
|
-
|
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
|
300
|
-
|
245
|
+
def assert(context)
|
246
|
+
propagate_assert(context)
|
301
247
|
end
|
302
248
|
|
303
|
-
def
|
304
|
-
|
249
|
+
def retract(fact)
|
250
|
+
propagate_retract(fact)
|
305
251
|
end
|
306
252
|
|
307
|
-
def
|
308
|
-
|
309
|
-
|
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
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
406
|
-
|
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
|
411
|
-
#
|
412
|
-
#
|
413
|
-
#
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
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
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
def assert(
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
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
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
486
|
-
|
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
|
-
|
496
|
-
|
497
|
-
def
|
498
|
-
|
499
|
-
|
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
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
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
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
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
|
582
|
-
|
583
|
-
@
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
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
|
-
|
612
|
-
|
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
|
-
|
630
|
-
|
631
|
-
|
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
|
634
|
-
|
635
|
-
|
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
|
640
|
-
|
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
|
644
|
-
|
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
|
648
|
-
|
649
|
-
|
503
|
+
def retract_left(fact)
|
504
|
+
@left_memory.delete(fact.id)
|
505
|
+
propagate_retract(fact)
|
650
506
|
end
|
651
507
|
|
652
|
-
def
|
653
|
-
|
654
|
-
@
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
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:
|
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
|
-
|
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
|