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
@@ -0,0 +1,217 @@
|
|
1
|
+
module Ruleby
|
2
|
+
module Core
|
3
|
+
|
4
|
+
class Pattern
|
5
|
+
# a list of AtomTag objects
|
6
|
+
attr_reader :atom_tags
|
7
|
+
end
|
8
|
+
|
9
|
+
class ObjectPattern < Pattern
|
10
|
+
|
11
|
+
# 'head' is a class type
|
12
|
+
def initialize(tag, head, atoms)
|
13
|
+
init_vars tag, head, atoms
|
14
|
+
end
|
15
|
+
|
16
|
+
def init_vars(tag,head,atoms,match=true)
|
17
|
+
@head = head
|
18
|
+
@headatom = TypeAtom.new tag, :class do |t| t == head end
|
19
|
+
@atoms = [@headatom] + atoms
|
20
|
+
@atom_tags = @atoms.collect {|a| AtomTag.new a.tag, match}
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :head
|
24
|
+
attr_reader :atoms
|
25
|
+
attr_reader :atom_tags
|
26
|
+
|
27
|
+
def ==(pattern)
|
28
|
+
atoms = pattern.atoms
|
29
|
+
if(@atoms.size == atoms.size)
|
30
|
+
(0..@atoms.size).each do |i|
|
31
|
+
if !(@atoms[i] == atoms[i])
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
return true
|
36
|
+
end
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
return '(' + @atoms.join('|') + ')'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class NotPattern < ObjectPattern
|
46
|
+
def initialize(tag, head, atoms)
|
47
|
+
init_vars tag, head, atoms, false
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(pattern)
|
51
|
+
return pattern.kind_of?(NotPattern) && super==(pattern)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class CompositePattern < Pattern
|
56
|
+
|
57
|
+
def initialize(left_pattern, right_pattern)
|
58
|
+
@left_pattern = left_pattern
|
59
|
+
@right_pattern = right_pattern
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :left_pattern, :right_pattern
|
63
|
+
|
64
|
+
def atoms
|
65
|
+
atoms = []
|
66
|
+
atoms.push @left_pattern.atoms
|
67
|
+
atoms.push @right_pattern.atoms
|
68
|
+
return atoms
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class AndPattern < CompositePattern
|
73
|
+
|
74
|
+
def initialize(left_pattern, right_pattern)
|
75
|
+
super(left_pattern, right_pattern)
|
76
|
+
@head = :and
|
77
|
+
@atom_tags = left_pattern.atom_tags.concat(right_pattern.atom_tags)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
class OrPattern < CompositePattern
|
83
|
+
|
84
|
+
def initialize(left_pattern, right_pattern)
|
85
|
+
super(left_pattern, right_pattern)
|
86
|
+
@head = :or
|
87
|
+
@atom_tags = [[left_pattern.atom_tags, right_pattern.atom_tags]]
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
class AtomTag
|
93
|
+
def initialize(tag, exists=true)
|
94
|
+
@tag = tag
|
95
|
+
|
96
|
+
# QUESTION can we get rid of this property? if so, we may not need this
|
97
|
+
# class at all.
|
98
|
+
@exists = exists
|
99
|
+
end
|
100
|
+
attr_reader :tag,:exists
|
101
|
+
def to_s
|
102
|
+
return "[#{@tag.to_s}]"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class MatchContext
|
107
|
+
def initialize(fact_id=nil, match_results=[])
|
108
|
+
@match_hash = DoubleHash.new fact_id, match_results
|
109
|
+
end
|
110
|
+
|
111
|
+
def clear
|
112
|
+
@match_hash = DoubleHash.new
|
113
|
+
end
|
114
|
+
|
115
|
+
def add(ids,mr)
|
116
|
+
@match_hash.add ids, mr
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_uniq(ids,mr)
|
120
|
+
@match_hash.add_uniq ids, mr
|
121
|
+
end
|
122
|
+
|
123
|
+
def each_match_result
|
124
|
+
@match_hash.each do |ids,mr|
|
125
|
+
yield(ids,mr)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def each_fact_id
|
130
|
+
@match_hash.each_id do |id|
|
131
|
+
yield(id)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def contains?(mr)
|
136
|
+
return @match_hash.value?(mr)
|
137
|
+
end
|
138
|
+
|
139
|
+
def +(mc)
|
140
|
+
# TODO make better
|
141
|
+
new_mc = MatchContext.new
|
142
|
+
new_mc.match_hash = @match_hash + mc.match_hash
|
143
|
+
return new_mc
|
144
|
+
end
|
145
|
+
|
146
|
+
def concat(match_context)
|
147
|
+
@match_hash.concat match_context.match_hash
|
148
|
+
return self
|
149
|
+
end
|
150
|
+
|
151
|
+
def concat_uniq(match_context)
|
152
|
+
@match_hash.concat_uniq match_context.match_hash
|
153
|
+
return self
|
154
|
+
end
|
155
|
+
|
156
|
+
def default
|
157
|
+
return @match_hash.default
|
158
|
+
end
|
159
|
+
|
160
|
+
def remove(id)
|
161
|
+
return @match_hash.remove(id)
|
162
|
+
end
|
163
|
+
|
164
|
+
def delete_if
|
165
|
+
@match_hash.delete_if do |mr|
|
166
|
+
yield(mr)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def match_results
|
171
|
+
v = @match_hash.values
|
172
|
+
if v.kind_of?(Hash)
|
173
|
+
# QUESTION What the heck is going on here? Why did this return a Hash
|
174
|
+
# instead of an Array?
|
175
|
+
return v.values
|
176
|
+
end
|
177
|
+
return v
|
178
|
+
end
|
179
|
+
|
180
|
+
def dup
|
181
|
+
dup_mc = MatchContext.new
|
182
|
+
dup_mc.match_hash = @match_hash.dup
|
183
|
+
return dup_mc
|
184
|
+
end
|
185
|
+
|
186
|
+
def subset(fact_id)
|
187
|
+
mrs = @match_hash.values_by_id(fact_id)
|
188
|
+
return MatchContext.new(fact_id, mrs.dup)
|
189
|
+
end
|
190
|
+
|
191
|
+
def satisfied?
|
192
|
+
return !@match_hash.values.empty?
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_s
|
196
|
+
s = '[MatchContext('
|
197
|
+
each_match_result do |ids,mr|
|
198
|
+
s = s + "[#{ids.join(',')}->#{mr}]"
|
199
|
+
end
|
200
|
+
s = s + ')]'
|
201
|
+
end
|
202
|
+
|
203
|
+
def ==(mc)
|
204
|
+
return false if mc == nil
|
205
|
+
mc.match_hash.keys.each do |id|
|
206
|
+
return false if @match_hash.keys.index(id) == nil
|
207
|
+
end
|
208
|
+
mc.match_results.values.each do |mr|
|
209
|
+
return false if match_results.values.index(mr) == nil
|
210
|
+
end
|
211
|
+
return true
|
212
|
+
end
|
213
|
+
|
214
|
+
attr :match_hash, true
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
data/lib/core/utils.rb
ADDED
@@ -0,0 +1,390 @@
|
|
1
|
+
module Ruleby
|
2
|
+
module Core
|
3
|
+
|
4
|
+
# We use this custom structure for our data so that we can hash the nodes by
|
5
|
+
# the type they represent. This allows for faster results because we can limit
|
6
|
+
# the number of nodes we compare against. In addition, this class does some of
|
7
|
+
# the compiling of the rules when a node is added.
|
8
|
+
class NodeNetworkArray
|
9
|
+
def initialize
|
10
|
+
@nodes = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(clazz)
|
14
|
+
nodes = @nodes[clazz]
|
15
|
+
if nodes
|
16
|
+
nodes.each do |n|
|
17
|
+
yield n
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def each_internal
|
23
|
+
@nodes.values.flatten.each do |n|
|
24
|
+
yield n
|
25
|
+
end
|
26
|
+
end
|
27
|
+
private:each_internal
|
28
|
+
|
29
|
+
def has_node(node)
|
30
|
+
@nodes[node.pattern.head].each do |node2|
|
31
|
+
if node2 == node
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_internal(node)
|
39
|
+
nodes = @nodes[node.pattern.head]
|
40
|
+
nodes = [] unless nodes
|
41
|
+
nodes.push node
|
42
|
+
@nodes[node.pattern.head] = nodes
|
43
|
+
end
|
44
|
+
private:add_internal
|
45
|
+
|
46
|
+
# This method adds a node to our list. In addition, it creates bindings between
|
47
|
+
# this node and any nodes that it references or is referenced by
|
48
|
+
def add(object_node)
|
49
|
+
object_node.pattern.atoms.each do |atom|
|
50
|
+
if (atom.kind_of?(ReferenceAtom))
|
51
|
+
each_internal do |local_node|
|
52
|
+
added = false
|
53
|
+
atom.vars.each do |var|
|
54
|
+
local_node.pattern.atoms.each do |local_atom|
|
55
|
+
if (var == local_atom.tag)
|
56
|
+
atom.var_atoms[var] = local_atom
|
57
|
+
unless added
|
58
|
+
local_node.referenced_by.push object_node
|
59
|
+
added = true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
unless atom
|
67
|
+
# TODO it would be better to throw this exception before the
|
68
|
+
# the rule is asserted
|
69
|
+
raise "Reference not found for: " + atom.tag.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
add_internal(object_node);
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# This class is used when we need to have a Hash where keys and values are mapped
|
78
|
+
# many-to-many. This class allows for quick access of both key and value.
|
79
|
+
class DoubleHash
|
80
|
+
def initialize(fact_id=nil, values=[])
|
81
|
+
@i = 0
|
82
|
+
clear
|
83
|
+
if fact_id
|
84
|
+
@fact_ids = {fact_id => []}
|
85
|
+
values.each do |v|
|
86
|
+
key = generate_key()
|
87
|
+
key_list = @fact_ids[fact_id]
|
88
|
+
key_list.push key
|
89
|
+
@fact_ids[fact_id] = key_list
|
90
|
+
@values = {key => v}
|
91
|
+
@backward_hash = {key => [fact_id]}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def rehash
|
97
|
+
@fact_ids.rehash
|
98
|
+
@values.rehash
|
99
|
+
@backward_hash.rehash
|
100
|
+
end
|
101
|
+
|
102
|
+
def value?(mr)
|
103
|
+
@values.value?(mr)
|
104
|
+
end
|
105
|
+
|
106
|
+
def clear
|
107
|
+
@fact_ids = {}
|
108
|
+
@values = {}
|
109
|
+
@backward_hash = {}
|
110
|
+
end
|
111
|
+
|
112
|
+
def values_by_id(id)
|
113
|
+
keys = @fact_ids[id]
|
114
|
+
values = []
|
115
|
+
if keys
|
116
|
+
keys.each do |k|
|
117
|
+
values.push @values[k]
|
118
|
+
end
|
119
|
+
else
|
120
|
+
#???
|
121
|
+
end
|
122
|
+
return values
|
123
|
+
end
|
124
|
+
|
125
|
+
def each_id
|
126
|
+
@fact_ids.each_key do |id|
|
127
|
+
yield(id)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def has_id?(id)
|
132
|
+
return @fact_ids.has_key?(id)
|
133
|
+
end
|
134
|
+
|
135
|
+
def +(dh)
|
136
|
+
# TODO this can be much faster
|
137
|
+
new_dh = dh.dup
|
138
|
+
dh.concat self.dup
|
139
|
+
return new_dh
|
140
|
+
end
|
141
|
+
|
142
|
+
def add(ids,val)
|
143
|
+
key = generate_key()
|
144
|
+
ids.each do |id|
|
145
|
+
key_list = @fact_ids[id]
|
146
|
+
key_list = [] if key_list == @fact_ids.default
|
147
|
+
key_list.push key
|
148
|
+
@fact_ids[id] = key_list
|
149
|
+
end
|
150
|
+
@values[key] = val
|
151
|
+
@backward_hash[key] = ids
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_uniq(ids,val)
|
155
|
+
key = generate_key()
|
156
|
+
exist_list = []
|
157
|
+
ids.each do |id|
|
158
|
+
key_list = @fact_ids[id]
|
159
|
+
if key_list != @fact_ids.default
|
160
|
+
key_list.each do |existing_key|
|
161
|
+
existing_val = @values[existing_key]
|
162
|
+
if existing_val
|
163
|
+
if val == existing_val
|
164
|
+
key = existing_key
|
165
|
+
exist_list.push id
|
166
|
+
break
|
167
|
+
end
|
168
|
+
else
|
169
|
+
# HACK there shouldn't be any keys like this in the
|
170
|
+
# hash to being with. Why are they there?
|
171
|
+
key_list.delete(existing_key)
|
172
|
+
@fact_ids[id] = key_list
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
add_list = ids - exist_list
|
178
|
+
add_list.each do |id|
|
179
|
+
key_list = @fact_ids[id]
|
180
|
+
key_list = [] if key_list == @fact_ids.default
|
181
|
+
key_list.push key
|
182
|
+
@fact_ids[id] = key_list
|
183
|
+
end
|
184
|
+
@values[key] = val if exist_list.empty?
|
185
|
+
b_list = @backward_hash[key]
|
186
|
+
if b_list
|
187
|
+
@backward_hash[key] = b_list | ids
|
188
|
+
else
|
189
|
+
@backward_hash[key] = ids
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def each
|
194
|
+
@values.each do |key,val|
|
195
|
+
ids = @backward_hash[key]
|
196
|
+
yield(ids,val)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def each_internal
|
201
|
+
@values.each do |key,val|
|
202
|
+
ids = @backward_hash[key]
|
203
|
+
yield(ids,key,val)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
private:each_internal
|
207
|
+
|
208
|
+
def concat(double_hash)
|
209
|
+
double_hash.each do |ids,val|
|
210
|
+
add(ids,val)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def concat_uniq(double_hash)
|
215
|
+
double_hash.each do |ids,val|
|
216
|
+
add_uniq(ids,val)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def default
|
221
|
+
return @values.default
|
222
|
+
end
|
223
|
+
|
224
|
+
def remove(id)
|
225
|
+
key_list = @fact_ids.delete(id)
|
226
|
+
if key_list != @fact_ids.default
|
227
|
+
removed_values = []
|
228
|
+
key_list.each do |key|
|
229
|
+
value = @values.delete(key)
|
230
|
+
removed_values.push value
|
231
|
+
id_list = @backward_hash.delete(key)
|
232
|
+
id_list.each do |next_id|
|
233
|
+
remove_internal(next_id,key) if next_id != id
|
234
|
+
end
|
235
|
+
end
|
236
|
+
return removed_values
|
237
|
+
else
|
238
|
+
# puts 'WARN: tried to remove from DoubleHash where id does not exist'
|
239
|
+
return default
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def remove_internal(id,key)
|
244
|
+
key_list = @fact_ids[id]
|
245
|
+
if key_list # BUG this shouldn't be nil!
|
246
|
+
key_list.delete(key)
|
247
|
+
if key_list.empty?
|
248
|
+
@fact_ids.delete(id)
|
249
|
+
else
|
250
|
+
@fact_ids[id] = key_list
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
private:remove_internal
|
255
|
+
|
256
|
+
def remove_by_key(ids,key)
|
257
|
+
ids.each do |id|
|
258
|
+
key_list = @fact_ids[id]
|
259
|
+
key_list.delete(key)
|
260
|
+
if key_list.empty?
|
261
|
+
@fact_ids.delete(id)
|
262
|
+
else
|
263
|
+
@fact_ids[id] = key_list
|
264
|
+
end
|
265
|
+
end
|
266
|
+
@values.delete(key)
|
267
|
+
@backward_hash.delete(key)
|
268
|
+
end
|
269
|
+
private:remove_by_key
|
270
|
+
|
271
|
+
def delete_if
|
272
|
+
@values.delete_if do |key,v|
|
273
|
+
if yield(v)
|
274
|
+
id_list = @backward_hash.delete(key)
|
275
|
+
id_list.each do |next_id|
|
276
|
+
remove_internal(next_id,key)
|
277
|
+
end
|
278
|
+
true
|
279
|
+
else
|
280
|
+
false
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def values
|
286
|
+
return @values.values
|
287
|
+
end
|
288
|
+
|
289
|
+
def keys
|
290
|
+
return @fact_ids.keys
|
291
|
+
end
|
292
|
+
|
293
|
+
def dup
|
294
|
+
dup_mc = DoubleHash.new
|
295
|
+
each do |ids,v|
|
296
|
+
dup_mc.add ids, v.dup
|
297
|
+
end
|
298
|
+
return dup_mc
|
299
|
+
end
|
300
|
+
|
301
|
+
def generate_key()
|
302
|
+
@i = @i + 1
|
303
|
+
return @i
|
304
|
+
end
|
305
|
+
private:generate_key
|
306
|
+
|
307
|
+
# This method is for testing. It ensures that all the Hash's
|
308
|
+
# and Array's are in order, and not corrupted (ex. some fact_id points
|
309
|
+
# to a key that does not exist in the match_results Hash).
|
310
|
+
def valid?
|
311
|
+
@fact_ids.each do |id,keys|
|
312
|
+
# key_list = @fact_ids[id]
|
313
|
+
# if key_list != @fact_ids.default
|
314
|
+
# key_list.each do |key|
|
315
|
+
# id_list = @backward_hash[key]
|
316
|
+
# unless id_list
|
317
|
+
# puts 'yup'
|
318
|
+
# return false
|
319
|
+
# end
|
320
|
+
# end
|
321
|
+
# end
|
322
|
+
keys.each do |key|
|
323
|
+
count = 0
|
324
|
+
keys.each do |key2|
|
325
|
+
if key == key2
|
326
|
+
count = count + 1
|
327
|
+
if count > 1
|
328
|
+
puts '(0) Duplicate keys in entry for fact_ids'
|
329
|
+
return false
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
mr = @match_results[key]
|
335
|
+
if mr == @match_results.default
|
336
|
+
puts '(1) Missing entry in @match_results for key'
|
337
|
+
return false
|
338
|
+
end
|
339
|
+
|
340
|
+
# @match_results.each do |mr_key,other_mr|
|
341
|
+
# if other_mr == mr && mr_key != key
|
342
|
+
# puts '(1a) Duplicate entry in @match_results'
|
343
|
+
# return false
|
344
|
+
# end
|
345
|
+
# end
|
346
|
+
|
347
|
+
id_list = @backward_hash[key]
|
348
|
+
if id_list == @backward_hash.default
|
349
|
+
puts '(2) Missing entry in backward_hash for key'
|
350
|
+
return false
|
351
|
+
end
|
352
|
+
|
353
|
+
if id_list.index(id) == nil
|
354
|
+
puts '(3) Entry in backward_hash is missing id'
|
355
|
+
return false
|
356
|
+
end
|
357
|
+
|
358
|
+
id_list.each do |ref_id|
|
359
|
+
unless ref_id == id
|
360
|
+
ref_key_list = @fact_ids[ref_id]
|
361
|
+
if ref_key_list == @fact_ids.default
|
362
|
+
puts '(4) Missing entry in fact_ids for backward_hash id'
|
363
|
+
puts "#{id},#{mr},#{key},#{ref_id}"
|
364
|
+
return false
|
365
|
+
end
|
366
|
+
|
367
|
+
if ref_key_list.index(key) == nil
|
368
|
+
puts '(5) Entry in fact_ids is missing key'
|
369
|
+
puts "#{id},#{mr},#{key},#{ref_id}"
|
370
|
+
return false
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
return true
|
377
|
+
end
|
378
|
+
private:valid?
|
379
|
+
|
380
|
+
def ==(dh)
|
381
|
+
# TODO need to implement this
|
382
|
+
return super
|
383
|
+
end
|
384
|
+
|
385
|
+
attr_reader :fact_ids
|
386
|
+
attr_reader :values
|
387
|
+
attr_reader :backward_hash
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|