ruleby 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/core/atoms.rb +226 -0
- data/lib/core/engine.rb +211 -0
- data/lib/core/nodes.rb +684 -0
- data/lib/core/patterns.rb +217 -0
- data/lib/core/utils.rb +390 -0
- data/lib/rulebook.rb +205 -0
- data/lib/ruleby.rb +29 -0
- metadata +51 -0
data/lib/core/atoms.rb
ADDED
@@ -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
|
data/lib/core/engine.rb
ADDED
@@ -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
|
data/lib/core/nodes.rb
ADDED
@@ -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
|