ruleby 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/core/atoms.rb +4 -0
- data/lib/core/engine.rb +9 -1
- data/lib/core/nodes.rb +16 -13
- data/lib/core/patterns.rb +10 -1
- data/lib/core/utils.rb +372 -365
- data/lib/dsl/treetop/treetop_helper.rb +23 -0
- data/lib/dsl/treetop/tt_dsl.treetop +105 -0
- data/tests/test.rb +3 -0
- metadata +8 -5
data/lib/core/atoms.rb
CHANGED
data/lib/core/engine.rb
CHANGED
@@ -182,6 +182,7 @@ module Ruleby
|
|
182
182
|
@working_memory = wm
|
183
183
|
@conflict_resolver = cr
|
184
184
|
@wm_altered = false
|
185
|
+
assert InitialFact.new
|
185
186
|
end
|
186
187
|
|
187
188
|
def facts
|
@@ -207,9 +208,16 @@ module Ruleby
|
|
207
208
|
assert(object,&block)
|
208
209
|
end
|
209
210
|
|
211
|
+
def retrieve(c)
|
212
|
+
facts.select {|f| f.kind_of?(c)}
|
213
|
+
end
|
214
|
+
|
210
215
|
# This method adds a new rule to the system.
|
211
216
|
def assert_rule(rule)
|
212
|
-
|
217
|
+
if @root == nil
|
218
|
+
@root = RootNode.new(@working_memory)
|
219
|
+
@root.reset_counter
|
220
|
+
end
|
213
221
|
@root.assert_rule rule
|
214
222
|
end
|
215
223
|
|
data/lib/core/nodes.rb
CHANGED
@@ -28,9 +28,8 @@ module Ruleby
|
|
28
28
|
# This method is invoked when a new rule is added to the system. The
|
29
29
|
# rule is processed and the appropriate nodes are added to the network.
|
30
30
|
def assert_rule(rule)
|
31
|
-
pattern = rule.pattern
|
32
31
|
terminal_node = TerminalNode.new rule
|
33
|
-
build_network(pattern, terminal_node)
|
32
|
+
build_network(rule.pattern, terminal_node)
|
34
33
|
@terminal_nodes.push terminal_node
|
35
34
|
end
|
36
35
|
|
@@ -96,10 +95,16 @@ module Ruleby
|
|
96
95
|
# Returns a new node in the network that wraps the given pattern and
|
97
96
|
# is above (i.e. it outputs to) the given node.
|
98
97
|
def build_network(pattern, out_node, side=nil)
|
99
|
-
if
|
100
|
-
|
101
|
-
|
102
|
-
|
98
|
+
if pattern.kind_of?(ObjectPattern)
|
99
|
+
if pattern.kind_of?(NotPattern) and (side==:left or !side)
|
100
|
+
# a NotPattern needs to be run through a NotNode, which is a beta node.
|
101
|
+
# So if the NotPattern is on the left (i.e. it is the first pattern),
|
102
|
+
# then we need to add a dummy pattern in front of it.
|
103
|
+
new_pattern = CompositePattern.new(InitialFactPattern.new, pattern)
|
104
|
+
return build_network(new_pattern, out_node, side)
|
105
|
+
else
|
106
|
+
return create_atom_nodes(pattern, out_node, side)
|
107
|
+
end
|
103
108
|
else
|
104
109
|
join_node = create_join_node(pattern, out_node, side)
|
105
110
|
build_network(pattern.left_pattern, join_node, :left)
|
@@ -117,7 +122,7 @@ module Ruleby
|
|
117
122
|
# side - if the out_node is a JoinNode, this marks the side
|
118
123
|
def create_atom_nodes(pattern, out_node, side)
|
119
124
|
# TODO refactor this method so it clear and concise
|
120
|
-
type_node = create_type_node(pattern)
|
125
|
+
type_node = create_type_node(pattern)
|
121
126
|
forked = false
|
122
127
|
parent_atom = pattern.atoms[0]
|
123
128
|
parent_node = type_node
|
@@ -164,10 +169,7 @@ module Ruleby
|
|
164
169
|
# out_node - the Node that this pattern is directly above in thw network
|
165
170
|
# side - if the out_node is a JoinNode, this marks the side
|
166
171
|
def create_join_node(pattern, out_node, side)
|
167
|
-
|
168
|
-
if (pattern.left_pattern.kind_of?(NotPattern))
|
169
|
-
raise 'NotPatterns at the being of a rule are not yet supported'
|
170
|
-
elsif (pattern.right_pattern.kind_of?(NotPattern))
|
172
|
+
if (pattern.right_pattern.kind_of?(NotPattern))
|
171
173
|
join_node = NotNode.new
|
172
174
|
else
|
173
175
|
join_node = JoinNode.new
|
@@ -232,9 +234,10 @@ module Ruleby
|
|
232
234
|
# working memory. It can be a costly operation because it iterates over
|
233
235
|
# EVERY fact in working memory. It should only be used when a new rule is
|
234
236
|
# added.
|
235
|
-
def compare_to_wm(type_node)
|
237
|
+
def compare_to_wm(type_node)
|
236
238
|
@working_memory.each_fact do |fact|
|
237
|
-
type_node.
|
239
|
+
type_node.retract fact
|
240
|
+
type_node.assert fact
|
238
241
|
end
|
239
242
|
end
|
240
243
|
end
|
data/lib/core/patterns.rb
CHANGED
@@ -99,9 +99,18 @@ module Ruleby
|
|
99
99
|
end
|
100
100
|
|
101
101
|
end
|
102
|
+
|
103
|
+
class InitialFactPattern < ObjectPattern
|
104
|
+
def initialize
|
105
|
+
deftemplate = DefTemplate.new InitialFact, :equals
|
106
|
+
htag = GeneratedTag.new
|
107
|
+
head = HeadAtom.new htag, deftemplate
|
108
|
+
super(head, [])
|
109
|
+
end
|
110
|
+
end
|
102
111
|
|
103
112
|
class PatternFactory
|
104
|
-
|
113
|
+
# TODO add some convenience methods for creating patterns
|
105
114
|
end
|
106
115
|
end
|
107
116
|
end
|
data/lib/core/utils.rb
CHANGED
@@ -11,454 +11,461 @@
|
|
11
11
|
|
12
12
|
module Ruleby
|
13
13
|
module Core
|
14
|
+
|
15
|
+
# This class is used as a unique fact that is assert to an engine's working memory
|
16
|
+
# immediately after creation. This fact is used mainly when a NotPattern is put
|
17
|
+
# at the begining of a rule. This allows it to join the 'not' to something tangible.
|
18
|
+
class InitialFact
|
19
|
+
|
20
|
+
end
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
22
|
+
# This class is a wrapper for the context under which the network executes for
|
23
|
+
# for a given fact. It is essentially a wrapper for a fact and a partial
|
24
|
+
# match.
|
25
|
+
class MatchContext
|
19
26
|
|
20
|
-
|
21
|
-
|
27
|
+
attr_reader:fact
|
28
|
+
attr_reader:match
|
22
29
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
30
|
+
def initialize(fact,mr)
|
31
|
+
@fact = fact
|
32
|
+
@match = mr
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
35
|
+
def to_s
|
36
|
+
return @match.to_s
|
37
|
+
end
|
31
38
|
|
32
|
-
|
33
|
-
|
39
|
+
def ==(t)
|
40
|
+
return t && @fact == t.fact && @match == t.match
|
41
|
+
end
|
34
42
|
end
|
35
|
-
end
|
36
43
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
# This class represents a partial match. It contains the variables, values,
|
45
|
+
# and some metadata about the match. For the most part, this metadata is used
|
46
|
+
# during conflict resolution.
|
47
|
+
class MatchResult
|
48
|
+
# TODO this class needs to be cleaned up so that we don't have a bunch of
|
49
|
+
# properties. Instead, maybe it sould have a list of facts.
|
43
50
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
51
|
+
attr :variables, true
|
52
|
+
attr :is_match, true
|
53
|
+
attr :fact_hash, true
|
54
|
+
attr :recency, true
|
48
55
|
|
49
|
-
|
50
|
-
|
56
|
+
def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[])
|
57
|
+
@variables = variables
|
51
58
|
|
52
|
-
|
53
|
-
|
59
|
+
# a list of recencies of the facts that this matchresult depends on.
|
60
|
+
@recency = recency
|
54
61
|
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
# notes where this match result is from a NotPattern or ObjectPattern
|
63
|
+
# TODO this isn't really needed anymore. how can we get rid of it?
|
64
|
+
@is_match = is_match
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
|
66
|
+
# a hash of fact.ids that each tag corresponds to
|
67
|
+
@fact_hash = fact_hash
|
68
|
+
end
|
62
69
|
|
63
|
-
|
64
|
-
|
65
|
-
|
70
|
+
def []=(sym, object)
|
71
|
+
@variables[sym] = object
|
72
|
+
end
|
66
73
|
|
67
|
-
|
68
|
-
|
69
|
-
|
74
|
+
def [](sym)
|
75
|
+
return @variables[sym]
|
76
|
+
end
|
70
77
|
|
71
|
-
|
72
|
-
|
73
|
-
|
78
|
+
def fact_ids
|
79
|
+
return fact_hash.values.uniq
|
80
|
+
end
|
74
81
|
|
75
|
-
|
76
|
-
|
77
|
-
|
82
|
+
def ==(match)
|
83
|
+
return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash
|
84
|
+
end
|
78
85
|
|
79
|
-
|
80
|
-
|
81
|
-
|
86
|
+
def key?(m)
|
87
|
+
return @variables.key?(m)
|
88
|
+
end
|
82
89
|
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
def keys
|
91
|
+
return @variables.keys
|
92
|
+
end
|
86
93
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
+
def update(mr)
|
95
|
+
@recency = @recency | mr.recency
|
96
|
+
@is_match = mr.is_match
|
97
|
+
@variables = @variables.update mr.variables
|
98
|
+
@fact_hash = @fact_hash.update mr.fact_hash
|
99
|
+
return self
|
100
|
+
end
|
94
101
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
102
|
+
def dup
|
103
|
+
dup_mr = MatchResult.new
|
104
|
+
dup_mr.recency = @recency.clone
|
105
|
+
dup_mr.is_match = @is_match
|
106
|
+
dup_mr.variables = @variables.clone
|
107
|
+
dup_mr.fact_hash = @fact_hash.clone
|
108
|
+
return dup_mr
|
109
|
+
end
|
103
110
|
|
104
|
-
|
105
|
-
|
106
|
-
|
111
|
+
def merge!(mr)
|
112
|
+
return update(mr)
|
113
|
+
end
|
107
114
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
115
|
+
def merge(mr)
|
116
|
+
new_mr = MatchResult.new
|
117
|
+
new_mr.recency = @recency | mr.recency
|
118
|
+
new_mr.is_match = mr.is_match
|
119
|
+
new_mr.variables = @variables.merge mr.variables
|
120
|
+
new_mr.fact_hash = @fact_hash.merge mr.fact_hash
|
121
|
+
return new_mr
|
122
|
+
end
|
116
123
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
def clear
|
125
|
+
@variables = {}
|
126
|
+
@fact_hash = {}
|
127
|
+
@recency = []
|
128
|
+
end
|
122
129
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
130
|
+
def delete(tag)
|
131
|
+
@variables.delete(tag)
|
132
|
+
@fact_hash.delete(tag)
|
133
|
+
end
|
127
134
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
135
|
+
def to_s
|
136
|
+
s = '#MatchResult('
|
137
|
+
s = s + 'f)(' unless @is_match
|
138
|
+
s = s + object_id.to_s+')('
|
139
|
+
@variables.each do |key,value|
|
140
|
+
s += "#{key}=#{value}/#{@fact_hash[key]}, "
|
141
|
+
end
|
142
|
+
return s + ")"
|
134
143
|
end
|
135
|
-
return s + ")"
|
136
144
|
end
|
137
|
-
end
|
138
145
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
146
|
+
# This class is used when we need to have a Hash where keys and values are
|
147
|
+
# mapped many-to-many. This class allows for quick access of both key and
|
148
|
+
# value. It is similar to Multimap in C++ standard lib.
|
149
|
+
# This thing is a mess (and barely works). It needs to be refactored.
|
150
|
+
class MultiHash
|
151
|
+
def initialize(key=nil, values=[])
|
152
|
+
@i = 0
|
153
|
+
clear
|
154
|
+
if key
|
155
|
+
@keys = {key => []}
|
156
|
+
values.each do |v|
|
157
|
+
xref = generate_xref()
|
158
|
+
xref_list = @keys[key]
|
159
|
+
xref_list.push xref
|
160
|
+
@keys[key] = xref_list
|
161
|
+
@values = {xref => v}
|
162
|
+
@backward_hash = {xref => [key]}
|
163
|
+
end
|
156
164
|
end
|
157
165
|
end
|
158
|
-
end
|
159
166
|
|
160
|
-
|
161
|
-
|
162
|
-
|
167
|
+
def empty?
|
168
|
+
return @keys.empty?
|
169
|
+
end
|
163
170
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
171
|
+
def rehash
|
172
|
+
@keys.rehash
|
173
|
+
@values.rehash
|
174
|
+
@backward_hash.rehash
|
175
|
+
end
|
169
176
|
|
170
|
-
|
171
|
-
|
172
|
-
|
177
|
+
def value?(mr)
|
178
|
+
@values.value?(mr)
|
179
|
+
end
|
173
180
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
181
|
+
def clear
|
182
|
+
@keys = {}
|
183
|
+
@values = {}
|
184
|
+
@backward_hash = {}
|
185
|
+
end
|
179
186
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
187
|
+
def values_by_id(id)
|
188
|
+
xrefs = @keys[id]
|
189
|
+
values = []
|
190
|
+
if xrefs
|
191
|
+
xrefs.each do |k|
|
192
|
+
values.push @values[k]
|
193
|
+
end
|
194
|
+
else
|
195
|
+
#???
|
186
196
|
end
|
187
|
-
|
188
|
-
#???
|
197
|
+
return values
|
189
198
|
end
|
190
|
-
return values
|
191
|
-
end
|
192
199
|
|
193
|
-
|
194
|
-
|
195
|
-
|
200
|
+
def each_key
|
201
|
+
@keys.each_key do |key|
|
202
|
+
yield(key)
|
203
|
+
end
|
196
204
|
end
|
197
|
-
end
|
198
205
|
|
199
|
-
|
200
|
-
|
201
|
-
|
206
|
+
def has_key?(key)
|
207
|
+
return @keys.has_key?(key)
|
208
|
+
end
|
202
209
|
|
203
|
-
|
204
|
-
|
205
|
-
|
210
|
+
def key?(key)
|
211
|
+
return has_key?(key)
|
212
|
+
end
|
206
213
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
214
|
+
def +(dh)
|
215
|
+
# TODO this can be faster
|
216
|
+
new_dh = dh.dup
|
217
|
+
dh.concat self.dup
|
218
|
+
return new_dh
|
219
|
+
end
|
213
220
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
221
|
+
def add(ids,val)
|
222
|
+
xref = generate_xref()
|
223
|
+
ids.each do |id|
|
224
|
+
xref_list = @keys[id]
|
225
|
+
xref_list = [] if xref_list == @keys.default
|
226
|
+
xref_list.push xref
|
227
|
+
@keys[id] = xref_list
|
228
|
+
end
|
229
|
+
@values[xref] = val
|
230
|
+
@backward_hash[xref] = ids
|
231
|
+
end
|
225
232
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
233
|
+
# DEPRECATED
|
234
|
+
# WARN this method adds a value to the MultiHash only if it is unique. It
|
235
|
+
# can be a fairly costly operation, and should be avoided. We only
|
236
|
+
# implemented this as part of a hack to get things working early on.
|
237
|
+
def add_uniq(ids,val)
|
238
|
+
xref = generate_xref()
|
239
|
+
exist_list = []
|
240
|
+
ids.each do |id|
|
241
|
+
xref_list = @keys[id]
|
242
|
+
if xref_list != @keys.default
|
243
|
+
xref_list.each do |existing_xref|
|
244
|
+
existing_val = @values[existing_xref]
|
245
|
+
if existing_val
|
246
|
+
if val == existing_val
|
247
|
+
xref = existing_xref
|
248
|
+
exist_list.push id
|
249
|
+
break
|
250
|
+
end
|
251
|
+
else
|
252
|
+
# HACK there shouldn't be any xrefs like this in the
|
253
|
+
# hash to being with. Why are they there?
|
254
|
+
xref_list.delete(existing_xref)
|
255
|
+
@keys[id] = xref_list
|
243
256
|
end
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
b_list = @backward_hash[xref]
|
262
|
-
if b_list
|
263
|
-
@backward_hash[xref] = b_list | ids
|
264
|
-
else
|
265
|
-
@backward_hash[xref] = ids
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
add_list = ids - exist_list
|
261
|
+
add_list.each do |id|
|
262
|
+
xref_list = @keys[id]
|
263
|
+
xref_list = [] if xref_list == @keys.default
|
264
|
+
xref_list.push xref
|
265
|
+
@keys[id] = xref_list
|
266
|
+
end
|
267
|
+
@values[xref] = val if exist_list.empty?
|
268
|
+
b_list = @backward_hash[xref]
|
269
|
+
if b_list
|
270
|
+
@backward_hash[xref] = b_list | ids
|
271
|
+
else
|
272
|
+
@backward_hash[xref] = ids
|
273
|
+
end
|
266
274
|
end
|
267
|
-
end
|
268
275
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
276
|
+
def each
|
277
|
+
@values.each do |xref,val|
|
278
|
+
ids = @backward_hash[xref]
|
279
|
+
yield(ids,val)
|
280
|
+
end
|
273
281
|
end
|
274
|
-
end
|
275
282
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
+
def each_internal
|
284
|
+
@values.each do |xref,val|
|
285
|
+
ids = @backward_hash[xref]
|
286
|
+
yield(ids,xref,val)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
private:each_internal
|
283
290
|
|
284
|
-
|
285
|
-
|
286
|
-
|
291
|
+
def concat(multi_hash)
|
292
|
+
multi_hash.each do |ids,val|
|
293
|
+
add(ids,val)
|
294
|
+
end
|
287
295
|
end
|
288
|
-
end
|
289
296
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
297
|
+
# DEPRECATED
|
298
|
+
# WARN see comments in add_uniq
|
299
|
+
def concat_uniq(double_hash)
|
300
|
+
double_hash.each do |ids,val|
|
301
|
+
add_uniq(ids,val)
|
302
|
+
end
|
295
303
|
end
|
296
|
-
end
|
297
304
|
|
298
|
-
|
299
|
-
|
300
|
-
|
305
|
+
def default
|
306
|
+
return @values.default
|
307
|
+
end
|
301
308
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
309
|
+
def remove(id)
|
310
|
+
xref_list = @keys.delete(id)
|
311
|
+
if xref_list != @keys.default
|
312
|
+
removed_values = []
|
313
|
+
xref_list.each do |xref|
|
314
|
+
value = @values.delete(xref)
|
315
|
+
removed_values.push value
|
316
|
+
id_list = @backward_hash.delete(xref)
|
317
|
+
id_list.each do |next_id|
|
318
|
+
remove_internal(next_id,xref) if next_id != id
|
319
|
+
end
|
312
320
|
end
|
321
|
+
return removed_values
|
322
|
+
else
|
323
|
+
# puts 'WARN: tried to remove from MultiHash where id does not exist'
|
324
|
+
return default
|
313
325
|
end
|
314
|
-
return removed_values
|
315
|
-
else
|
316
|
-
# puts 'WARN: tried to remove from MultiHash where id does not exist'
|
317
|
-
return default
|
318
326
|
end
|
319
|
-
end
|
320
327
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
328
|
+
def remove_internal(id,xref)
|
329
|
+
xref_list = @keys[id]
|
330
|
+
if xref_list # BUG this shouldn't be nil!
|
331
|
+
xref_list.delete(xref)
|
332
|
+
if xref_list.empty?
|
333
|
+
@keys.delete(id)
|
334
|
+
else
|
335
|
+
@keys[id] = xref_list
|
336
|
+
end
|
329
337
|
end
|
330
338
|
end
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
339
|
+
private:remove_internal
|
340
|
+
|
341
|
+
def remove_by_xref(ids,xref)
|
342
|
+
ids.each do |id|
|
343
|
+
xref_list = @keys[id]
|
344
|
+
xref_list.delete(xref)
|
345
|
+
if xref_list.empty?
|
346
|
+
@keys.delete(id)
|
347
|
+
else
|
348
|
+
@keys[id] = xref_list
|
349
|
+
end
|
342
350
|
end
|
351
|
+
@values.delete(xref)
|
352
|
+
@backward_hash.delete(xref)
|
343
353
|
end
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
354
|
+
private:remove_by_xref
|
355
|
+
|
356
|
+
def delete_if
|
357
|
+
@values.delete_if do |xref,v|
|
358
|
+
if yield(v)
|
359
|
+
id_list = @backward_hash.delete(xref)
|
360
|
+
id_list.each do |next_id|
|
361
|
+
remove_internal(next_id,xref)
|
362
|
+
end
|
363
|
+
true
|
364
|
+
else
|
365
|
+
false
|
355
366
|
end
|
356
|
-
true
|
357
|
-
else
|
358
|
-
false
|
359
367
|
end
|
360
368
|
end
|
361
|
-
end
|
362
369
|
|
363
|
-
|
364
|
-
|
365
|
-
|
370
|
+
def values
|
371
|
+
return @values.values
|
372
|
+
end
|
366
373
|
|
367
|
-
|
368
|
-
|
369
|
-
|
374
|
+
def keys
|
375
|
+
return @keys.keys
|
376
|
+
end
|
370
377
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
378
|
+
def dup
|
379
|
+
dup_mc = MultiHash.new
|
380
|
+
each do |ids,v|
|
381
|
+
dup_mc.add ids, v.dup
|
382
|
+
end
|
383
|
+
return dup_mc
|
375
384
|
end
|
376
|
-
return dup_mc
|
377
|
-
end
|
378
385
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
# xref_list = @keys[id]
|
391
|
-
# if xref_list != @keys.default
|
392
|
-
# xref_list.each do |xref|
|
393
|
-
# id_list = @backward_hash[xref]
|
394
|
-
# unless id_list
|
395
|
-
# puts 'yup'
|
396
|
-
# return false
|
397
|
-
# end
|
398
|
-
# end
|
399
|
-
# end
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
386
|
+
def generate_xref()
|
387
|
+
@i = @i + 1
|
388
|
+
return @i
|
389
|
+
end
|
390
|
+
private:generate_xref
|
391
|
+
|
392
|
+
# This method is for testing. It ensures that all the Hash's
|
393
|
+
# and Array's are in order, and not corrupted (ex. some key points
|
394
|
+
# to a xref that does not exist in the match_results Hash).
|
395
|
+
def valid?
|
396
|
+
@keys.each do |id,xrefs|
|
397
|
+
# xref_list = @keys[id]
|
398
|
+
# if xref_list != @keys.default
|
399
|
+
# xref_list.each do |xref|
|
400
|
+
# id_list = @backward_hash[xref]
|
401
|
+
# unless id_list
|
402
|
+
# puts 'yup'
|
403
|
+
# return false
|
404
|
+
# end
|
405
|
+
# end
|
406
|
+
# end
|
407
|
+
xrefs.each do |xref|
|
408
|
+
count = 0
|
409
|
+
xrefs.each do |xref2|
|
410
|
+
if xref == xref2
|
411
|
+
count = count + 1
|
412
|
+
if count > 1
|
413
|
+
puts '(0) Duplicate xrefs in entry for keys'
|
414
|
+
return false
|
415
|
+
end
|
408
416
|
end
|
409
417
|
end
|
410
|
-
end
|
411
418
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
419
|
+
mr = @match_results[xref]
|
420
|
+
if mr == @match_results.default
|
421
|
+
puts '(1) Missing entry in @match_results for xref'
|
422
|
+
return false
|
423
|
+
end
|
417
424
|
|
418
|
-
# @match_results.each do |mr_xref,other_mr|
|
419
|
-
# if other_mr == mr && mr_xref != xref
|
420
|
-
# puts '(1a) Duplicate entry in @match_results'
|
421
|
-
# return false
|
422
|
-
# end
|
423
|
-
# end
|
425
|
+
# @match_results.each do |mr_xref,other_mr|
|
426
|
+
# if other_mr == mr && mr_xref != xref
|
427
|
+
# puts '(1a) Duplicate entry in @match_results'
|
428
|
+
# return false
|
429
|
+
# end
|
430
|
+
# end
|
424
431
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
432
|
+
id_list = @backward_hash[xref]
|
433
|
+
if id_list == @backward_hash.default
|
434
|
+
puts '(2) Missing entry in backward_hash for xref'
|
435
|
+
return false
|
436
|
+
end
|
430
437
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
438
|
+
if id_list.index(id) == nil
|
439
|
+
puts '(3) Entry in backward_hash is missing id'
|
440
|
+
return false
|
441
|
+
end
|
435
442
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
443
|
+
id_list.each do |ref_id|
|
444
|
+
unless ref_id == id
|
445
|
+
ref_xref_list = @keys[ref_id]
|
446
|
+
if ref_xref_list == @keys.default
|
447
|
+
puts '(4) Missing entry in keys for backward_hash id'
|
448
|
+
puts "#{id},#{mr},#{xref},#{ref_id}"
|
449
|
+
return false
|
450
|
+
end
|
444
451
|
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
452
|
+
if ref_xref_list.index(xref) == nil
|
453
|
+
puts '(5) Entry in keys is missing xref'
|
454
|
+
puts "#{id},#{mr},#{xref},#{ref_id}"
|
455
|
+
return false
|
456
|
+
end
|
449
457
|
end
|
450
458
|
end
|
451
459
|
end
|
452
|
-
end
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
private:valid?
|
460
|
+
end
|
461
|
+
return true
|
462
|
+
end
|
463
|
+
private:valid?
|
457
464
|
|
458
|
-
|
459
|
-
|
460
|
-
|
465
|
+
def ==(dh)
|
466
|
+
# TODO need to implement this
|
467
|
+
return super
|
468
|
+
end
|
461
469
|
end
|
462
470
|
end
|
463
|
-
end
|
464
471
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
module Ruleby
|
3
|
+
class TreetopHelper
|
4
|
+
|
5
|
+
def self.rule(name, *args, &block)
|
6
|
+
options = args[0].kind_of?(Hash) ? args.shift : {}
|
7
|
+
|
8
|
+
r = Ferrari::RuleBuilder.new name
|
9
|
+
args.each do |arg|
|
10
|
+
if arg.kind_of? Array
|
11
|
+
r.when(*arg)
|
12
|
+
else
|
13
|
+
raise 'Invalid condition. All or none must be Arrays.'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
r.then(&block)
|
18
|
+
r.priority = options[:priority] if options[:priority]
|
19
|
+
|
20
|
+
return r.build_rule
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
grammar Ruleby
|
2
|
+
rule root
|
3
|
+
defrule* {
|
4
|
+
def get_rules
|
5
|
+
rs = []
|
6
|
+
elements.each do |e|
|
7
|
+
rs << e.get_rule()
|
8
|
+
end
|
9
|
+
return rs
|
10
|
+
end
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
rule defrule
|
15
|
+
'rule' space name space foreach space action {
|
16
|
+
def get_rule()
|
17
|
+
action_text = action.text_value.strip
|
18
|
+
action_text.gsub!('do', '')
|
19
|
+
action_text.gsub!('end', '')
|
20
|
+
return foreach.get_rule(name.text_value.strip, action_text.strip)
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
rule name
|
26
|
+
[a-zA-Z] [a-zA-Z0-9]*
|
27
|
+
end
|
28
|
+
|
29
|
+
rule foreach
|
30
|
+
'foreach' space head space symbol {
|
31
|
+
def get_rule(name, action_text)
|
32
|
+
class_name = head.text_value.strip
|
33
|
+
clazz = eval(class_name)
|
34
|
+
tag = symbol.text_value.strip
|
35
|
+
tag = tag[1, tag.size - 1]
|
36
|
+
return Ruleby::TreetopHelper.rule(name, [clazz, tag.to_sym], &action_text)
|
37
|
+
end
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
rule action
|
43
|
+
'do' space (!'end' .)* 'end' space?
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
rule head
|
49
|
+
[A-Z] [a-zA-Z]*
|
50
|
+
end
|
51
|
+
rule symbol
|
52
|
+
':' [a-zA-Z]+
|
53
|
+
end
|
54
|
+
rule where
|
55
|
+
'where'
|
56
|
+
end
|
57
|
+
rule method
|
58
|
+
symbol '.' [a-z] [a-zA-Z0-9]*
|
59
|
+
end
|
60
|
+
rule string
|
61
|
+
'\'' [a-zA-Z0-9]* '\''
|
62
|
+
end
|
63
|
+
rule value
|
64
|
+
method / '-'* [0-9]+ / symbol / string
|
65
|
+
end
|
66
|
+
rule expression
|
67
|
+
value space (equal space value) / (not_equal space value) / (symbol)
|
68
|
+
end
|
69
|
+
|
70
|
+
rule clause
|
71
|
+
expression more:(and_sign clause)* {
|
72
|
+
def populate
|
73
|
+
more.elements.each do |e|
|
74
|
+
e.clause.populate
|
75
|
+
end
|
76
|
+
end
|
77
|
+
}
|
78
|
+
end
|
79
|
+
rule and_sign
|
80
|
+
'AND'
|
81
|
+
end
|
82
|
+
rule not_equal
|
83
|
+
'!='
|
84
|
+
end
|
85
|
+
rule equal
|
86
|
+
'=='
|
87
|
+
end
|
88
|
+
|
89
|
+
rule space
|
90
|
+
white+
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
rule white
|
95
|
+
blank / eol
|
96
|
+
end
|
97
|
+
|
98
|
+
rule blank
|
99
|
+
[ \t]
|
100
|
+
end
|
101
|
+
|
102
|
+
rule eol
|
103
|
+
("\r" "\n"?) / "\n"
|
104
|
+
end
|
105
|
+
end
|
data/tests/test.rb
CHANGED
metadata
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruleby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "0.
|
4
|
+
version: "0.5"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Kutner
|
8
8
|
- Matt Smith
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date:
|
13
|
+
date: 2009-06-16 00:00:00 -05:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
17
|
-
description:
|
17
|
+
description: Ruleby is a rule engine written in the Ruby language. It is a system for executing a set of IF-THEN statements known as production rules. These rules are matched to objects using the forward chaining Rete algorithm. Ruleby provides an internal Domain Specific Language (DSL) for building the productions that make up a Ruleby program. Release Notes for Version 0.5 * reset the TerminalNode.counter when initializing the RootNode. Thanks to Shashank for the patch. * added InitialFact so that NotPatterns can be put at the front of a rule [#1 status:resolved] * Improved rule assertion so that rules can be added after facts have been asserted [#9 state:resolved] * Added retrieve method to engine, and a unit-test for it
|
18
18
|
email: matt@ruleby.org
|
19
19
|
executables: []
|
20
20
|
|
@@ -33,6 +33,9 @@ files:
|
|
33
33
|
- lib/dsl/ferrari.rb
|
34
34
|
- lib/dsl/letigre.rb
|
35
35
|
- lib/dsl/steel.rb
|
36
|
+
- lib/dsl/treetop
|
37
|
+
- lib/dsl/treetop/treetop_helper.rb
|
38
|
+
- lib/dsl/treetop/tt_dsl.treetop
|
36
39
|
- lib/dsl/yaml_dsl.rb
|
37
40
|
- lib/rulebook.rb
|
38
41
|
- lib/ruleby.rb
|
@@ -57,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
60
|
version:
|
58
61
|
requirements: []
|
59
62
|
|
60
|
-
rubyforge_project:
|
63
|
+
rubyforge_project: ruleby
|
61
64
|
rubygems_version: 1.0.1
|
62
65
|
signing_key:
|
63
66
|
specification_version: 2
|