ruleby 0.1
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/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
|