ruleby 0.2 → 0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/benchmarks/basic_rules.rb +66 -0
- data/benchmarks/joined_rules.rb +73 -0
- data/benchmarks/miss_manners/data.rb +11 -0
- data/benchmarks/miss_manners/miss_manners.rb +11 -1
- data/benchmarks/miss_manners/model.rb +11 -0
- data/benchmarks/miss_manners/rules.rb +49 -110
- data/benchmarks/model.rb +36 -0
- data/examples/example_diagnosis.rb +35 -73
- data/examples/example_hello.rb +20 -22
- data/examples/example_politician.rb +22 -11
- data/examples/example_ticket.rb +40 -85
- data/examples/fibonacci_example1.rb +13 -2
- data/examples/fibonacci_example2.rb +11 -0
- data/examples/fibonacci_rulebook.rb +58 -111
- data/examples/test_self_reference.rb +51 -9
- data/lib/core/atoms.rb +53 -116
- data/lib/core/engine.rb +96 -96
- data/lib/core/nodes.rb +330 -298
- data/lib/core/patterns.rb +36 -39
- data/lib/core/utils.rb +141 -3
- data/lib/dsl/ferrari.rb +263 -0
- data/lib/dsl/letigre.rb +212 -0
- data/lib/dsl/steel.rb +313 -0
- data/lib/rulebook.rb +82 -265
- data/lib/ruleby.rb +13 -0
- metadata +21 -19
- data/benchmarks/50_joined_rules.rb +0 -78
- data/benchmarks/50_rules.rb +0 -57
- data/benchmarks/5_joined_rules.rb +0 -78
- data/benchmarks/5_rules.rb +0 -57
data/lib/core/nodes.rb
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# This file is part of the Ruleby project (http://ruleby.org)
|
2
|
+
#
|
3
|
+
# This application is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the Ruby license defined in the
|
5
|
+
# LICENSE.txt file.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
|
8
|
+
#
|
9
|
+
# * Authors: Joe Kutner
|
10
|
+
#
|
11
|
+
|
1
12
|
module Ruleby
|
2
13
|
module Core
|
3
14
|
|
@@ -7,10 +18,7 @@ module Ruleby
|
|
7
18
|
class RootNode
|
8
19
|
def initialize(working_memory)
|
9
20
|
@working_memory = working_memory
|
10
|
-
|
11
|
-
# TODO once node sharing is implemented, @type_nodes will be a
|
12
|
-
# Class=>TypeNode Hash.
|
13
|
-
@type_nodes = []
|
21
|
+
@type_nodes = {}
|
14
22
|
@atom_nodes = []
|
15
23
|
@join_nodes = []
|
16
24
|
@terminal_nodes = []
|
@@ -20,171 +28,51 @@ module Ruleby
|
|
20
28
|
# rule is processed and the appropriate nodes are added to the network.
|
21
29
|
def assert_rule(rule)
|
22
30
|
pattern = rule.pattern
|
23
|
-
terminal_node = TerminalNode.new rule
|
31
|
+
terminal_node = TerminalNode.new rule
|
24
32
|
build_network(pattern, terminal_node)
|
25
33
|
@terminal_nodes.push terminal_node
|
26
|
-
end
|
27
|
-
|
28
|
-
# This method builds the network by starting at the bottom and recursively
|
29
|
-
# working its way to the top. The recursion goes up the left side of the
|
30
|
-
# tree first (depth first... but our tree is up-side-down).
|
31
|
-
# pattern - the pattern to process (Single or Composite)
|
32
|
-
# out_node - the node that will be below the new node in the network
|
33
|
-
# side - if the out_node is a JoinNode, this marks the side of the new node
|
34
|
-
# Returns a new node in the network that wraps the given pattern and
|
35
|
-
# is above (i.e. it outputs to) the given node.
|
36
|
-
def build_network(pattern, out_node, side=nil)
|
37
|
-
if (pattern.kind_of?(ObjectPattern))
|
38
|
-
atom_node = create_atom_nodes(pattern, out_node, side)
|
39
|
-
#out_node.parent_nodes.push atom_node # only used to print network
|
40
|
-
return atom_node
|
41
|
-
else
|
42
|
-
join_node = create_join_node(pattern, out_node, side)
|
43
|
-
build_network(pattern.left_pattern, join_node, :left)
|
44
|
-
build_network(pattern.right_pattern, join_node, :right)
|
45
|
-
out_node.parent_nodes.push join_node # only used to print network
|
46
|
-
return join_node
|
47
|
-
end
|
48
|
-
end
|
49
|
-
private:build_network
|
50
|
-
|
51
|
-
# This method is used to create the atom nodes that make up the given
|
52
|
-
# pattern's network. It returns the node that is at the top of the network
|
53
|
-
# for the pattern.
|
54
|
-
# pattern - the Pattern that the created node wraps
|
55
|
-
# out_node - the Node that this pattern is directly above in thw network
|
56
|
-
# side - if the out_node is a JoinNode, this marks the side of the new node
|
57
|
-
def create_atom_nodes(pattern, out_node, side)
|
58
|
-
type_node = create_type_node(pattern.atoms[0])
|
59
|
-
|
60
|
-
parent_node = type_node
|
61
|
-
for i in (1..(pattern.atoms.size-1))
|
62
|
-
node = nil
|
63
|
-
if pattern.atoms[i].kind_of?(SelfReferenceAtom)
|
64
|
-
node = create_self_reference_node(pattern.atoms[i])
|
65
|
-
elsif pattern.atoms[i].kind_of?(ReferenceAtom)
|
66
|
-
node = create_reference_node(pattern.atoms[i])
|
67
|
-
out_node.ref_nodes.push node
|
68
|
-
else
|
69
|
-
node = create_property_node(pattern.atoms[i])
|
70
|
-
end
|
71
|
-
parent_node.out_nodes.push node
|
72
|
-
node.parent_nodes.push parent_node
|
73
|
-
parent_node = node
|
74
|
-
end
|
75
|
-
out_node.parent_nodes.push parent_node
|
76
|
-
|
77
|
-
if out_node.kind_of?(JoinNode)
|
78
|
-
adapter_node = create_adapter_node(side)
|
79
|
-
parent_node.out_nodes.push adapter_node
|
80
|
-
parent_node = adapter_node
|
81
|
-
end
|
82
|
-
|
83
|
-
parent_node.out_nodes.push out_node
|
84
|
-
compare_to_wm(type_node)
|
85
|
-
return type_node
|
86
|
-
end
|
87
|
-
private:create_atom_nodes
|
88
|
-
|
89
|
-
# Creates a JoinNode, puts it at the middle of the network, and stores
|
90
|
-
# the node below it into its memory.
|
91
|
-
# pattern - the Pattern that the created node wraps
|
92
|
-
# out_node - the Node that this pattern is directly above in thw network
|
93
|
-
# side - if the out_node is a JoinNode, this marks the side of the new node
|
94
|
-
def create_join_node(pattern, out_node, side)
|
95
|
-
join_node = nil
|
96
|
-
if (pattern.left_pattern.kind_of?(NotPattern))
|
97
|
-
raise 'NotPatterns at the being of a rule are not yet supported'
|
98
|
-
elsif (pattern.right_pattern.kind_of?(NotPattern))
|
99
|
-
join_node = NotNode.new
|
100
|
-
else
|
101
|
-
join_node = JoinNode.new
|
102
|
-
end
|
103
|
-
|
104
|
-
@join_nodes.push(join_node)
|
105
|
-
parent_node = join_node
|
106
|
-
if out_node.kind_of?(JoinNode)
|
107
|
-
adapter_node = create_adapter_node(side)
|
108
|
-
parent_node.out_nodes.push adapter_node
|
109
|
-
parent_node = adapter_node
|
110
|
-
end
|
111
|
-
parent_node.out_nodes.push out_node
|
112
|
-
return join_node
|
113
|
-
end
|
114
|
-
private:create_join_node
|
115
|
-
|
116
|
-
def create_type_node(atom)
|
117
|
-
node = TypeNode.new atom
|
118
|
-
@type_nodes.push node
|
119
|
-
return node
|
120
|
-
end
|
121
|
-
private:create_type_node
|
122
|
-
|
123
|
-
def create_property_node(atom)
|
124
|
-
node = PropertyNode.new atom
|
125
|
-
@atom_nodes.push node
|
126
|
-
return node
|
127
|
-
end
|
128
|
-
private:create_property_node
|
129
|
-
|
130
|
-
def create_self_reference_node(atom)
|
131
|
-
node = SelfReferenceNode.new atom
|
132
|
-
@atom_nodes.push node
|
133
|
-
return node
|
134
|
-
end
|
135
|
-
private:create_self_reference_node
|
136
|
-
|
137
|
-
def create_reference_node(atom)
|
138
|
-
node = ReferenceNode.new atom
|
139
|
-
@atom_nodes.push node
|
140
|
-
return node
|
141
|
-
end
|
142
|
-
private:create_reference_node
|
143
|
-
|
144
|
-
def create_adapter_node(side)
|
145
|
-
if side == :left
|
146
|
-
return LeftAdapterNode.new
|
147
|
-
else
|
148
|
-
return RightAdapterNode.new
|
149
|
-
end
|
150
|
-
end
|
151
|
-
private:create_adapter_node
|
152
|
-
|
34
|
+
end
|
35
|
+
|
153
36
|
# When a new fact is added to working memory, or an existing one is removed
|
154
37
|
# this method is called. It finds any nodes that depend on it, and updates
|
155
38
|
# them accordingly.
|
156
39
|
def assert_fact(fact)
|
157
|
-
|
158
|
-
|
159
|
-
@type_nodes.each do |node|
|
40
|
+
node = @type_nodes[fact.object.class]
|
41
|
+
if node
|
160
42
|
if fact.token == :plus
|
161
|
-
node.assert(
|
43
|
+
node.assert(fact)
|
162
44
|
else
|
163
45
|
node.retract fact
|
164
46
|
end
|
165
47
|
end
|
166
48
|
end
|
49
|
+
|
50
|
+
# Increments the activation counter. This is just a pass-thru to the static
|
51
|
+
# variable in the terminal node
|
52
|
+
def increment_counter
|
53
|
+
TerminalNode.increment_counter
|
54
|
+
end
|
167
55
|
|
168
|
-
# This
|
169
|
-
#
|
170
|
-
|
171
|
-
|
172
|
-
def compare_to_wm(type_node)
|
173
|
-
@working_memory.each_fact do |fact|
|
174
|
-
type_node.assert MatchContext.new(fact)
|
175
|
-
end
|
56
|
+
# Resets the activation counter. This is just a pass-thru to the static
|
57
|
+
# variable in the terminal node
|
58
|
+
def reset_counter
|
59
|
+
TerminalNode.reset_counter
|
176
60
|
end
|
177
|
-
private:compare_to_wm
|
178
61
|
|
179
62
|
# When invoked, this method returns a list of all Action|MatchContext pairs
|
180
63
|
# as Activations. The list is generated when facts and rules are asserted,
|
181
64
|
# so no comparisions are done here (i.e. no Backward Chaining).
|
182
|
-
def
|
65
|
+
def matches(initial=true)
|
183
66
|
agenda = Array.new
|
184
67
|
@terminal_nodes.each do |node|
|
185
|
-
|
186
|
-
|
187
|
-
|
68
|
+
node.activations.values.each do |a|
|
69
|
+
if initial
|
70
|
+
a.used = false
|
71
|
+
agenda.push a
|
72
|
+
elsif !a.used
|
73
|
+
agenda.push a
|
74
|
+
end
|
75
|
+
end
|
188
76
|
end
|
189
77
|
return agenda
|
190
78
|
end
|
@@ -195,17 +83,165 @@ module Ruleby
|
|
195
83
|
node.print(' ')
|
196
84
|
end
|
197
85
|
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# This method builds the network by starting at the bottom and recursively
|
90
|
+
# working its way to the top. The recursion goes up the left side of the
|
91
|
+
# tree first (depth first... but our tree is up-side-down).
|
92
|
+
# pattern - the pattern to process (Single or Composite)
|
93
|
+
# out_node - the node that will be below the new node in the network
|
94
|
+
# side - if the out_node is a JoinNode, this marks the side
|
95
|
+
# Returns a new node in the network that wraps the given pattern and
|
96
|
+
# is above (i.e. it outputs to) the given node.
|
97
|
+
def build_network(pattern, out_node, side=nil)
|
98
|
+
if (pattern.kind_of?(ObjectPattern))
|
99
|
+
atom_node = create_atom_nodes(pattern, out_node, side)
|
100
|
+
#out_node.parent_nodes.push atom_node # only used to print network
|
101
|
+
return atom_node
|
102
|
+
else
|
103
|
+
join_node = create_join_node(pattern, out_node, side)
|
104
|
+
build_network(pattern.left_pattern, join_node, :left)
|
105
|
+
build_network(pattern.right_pattern, join_node, :right)
|
106
|
+
out_node.parent_nodes.push join_node # only used to print network
|
107
|
+
return join_node
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# This method is used to create the atom nodes that make up the given
|
112
|
+
# pattern's network. It returns the node that is at the top of the
|
113
|
+
# network for the pattern.
|
114
|
+
# pattern - the Pattern that the created node wraps
|
115
|
+
# out_node - the Node that this pattern is directly above in thw network
|
116
|
+
# side - if the out_node is a JoinNode, this marks the side
|
117
|
+
def create_atom_nodes(pattern, out_node, side)
|
118
|
+
# TODO refactor this method so it clear and concise
|
119
|
+
type_node = create_type_node(pattern)
|
120
|
+
|
121
|
+
parent_node = type_node
|
122
|
+
for i in (1..(pattern.atoms.size-1))
|
123
|
+
node = nil
|
124
|
+
atom = pattern.atoms[i]
|
125
|
+
if atom.kind_of?(SelfReferenceAtom)
|
126
|
+
node = create_self_reference_node(atom)
|
127
|
+
elsif atom.kind_of?(ReferenceAtom)
|
128
|
+
node = create_reference_node(atom)
|
129
|
+
out_node.ref_nodes.push node
|
130
|
+
else
|
131
|
+
node = create_property_node(atom)
|
132
|
+
end
|
133
|
+
parent_node.out_nodes.push node
|
134
|
+
node.parent_nodes.push parent_node
|
135
|
+
parent_node = node
|
136
|
+
end
|
137
|
+
|
138
|
+
bridge_node = create_bridge_node(pattern)
|
139
|
+
parent_node.out_nodes.push bridge_node
|
140
|
+
bridge_node.parent_nodes.push parent_node
|
141
|
+
parent_node = bridge_node
|
142
|
+
|
143
|
+
out_node.parent_nodes.push parent_node
|
144
|
+
|
145
|
+
if out_node.kind_of?(JoinNode)
|
146
|
+
adapter_node = create_adapter_node(side)
|
147
|
+
parent_node.out_nodes.push adapter_node
|
148
|
+
parent_node = adapter_node
|
149
|
+
end
|
150
|
+
|
151
|
+
parent_node.out_nodes.push out_node
|
152
|
+
compare_to_wm(type_node)
|
153
|
+
return type_node
|
154
|
+
end
|
155
|
+
|
156
|
+
# Creates a JoinNode, puts it at the middle of the network, and stores
|
157
|
+
# the node below it into its memory.
|
158
|
+
# pattern - the Pattern that the created node wraps
|
159
|
+
# out_node - the Node that this pattern is directly above in thw network
|
160
|
+
# side - if the out_node is a JoinNode, this marks the side
|
161
|
+
def create_join_node(pattern, out_node, side)
|
162
|
+
join_node = nil
|
163
|
+
if (pattern.left_pattern.kind_of?(NotPattern))
|
164
|
+
raise 'NotPatterns at the being of a rule are not yet supported'
|
165
|
+
elsif (pattern.right_pattern.kind_of?(NotPattern))
|
166
|
+
join_node = NotNode.new
|
167
|
+
else
|
168
|
+
join_node = JoinNode.new
|
169
|
+
end
|
170
|
+
|
171
|
+
@join_nodes.push(join_node)
|
172
|
+
parent_node = join_node
|
173
|
+
if out_node.kind_of?(JoinNode)
|
174
|
+
adapter_node = create_adapter_node(side)
|
175
|
+
parent_node.out_nodes.push adapter_node
|
176
|
+
parent_node = adapter_node
|
177
|
+
end
|
178
|
+
parent_node.out_nodes.push out_node
|
179
|
+
return join_node
|
180
|
+
end
|
181
|
+
|
182
|
+
def create_type_node(pattern)
|
183
|
+
atom = pattern.atoms[0]
|
184
|
+
node = @type_nodes[atom.clazz]
|
185
|
+
unless node
|
186
|
+
node = TypeNode.new atom
|
187
|
+
@type_nodes[atom.clazz] = node
|
188
|
+
end
|
189
|
+
return node
|
190
|
+
end
|
191
|
+
|
192
|
+
def create_bridge_node(pattern)
|
193
|
+
return BridgeNode.new(pattern)
|
194
|
+
end
|
195
|
+
|
196
|
+
def create_property_node(atom)
|
197
|
+
node = PropertyNode.new atom
|
198
|
+
@atom_nodes.push node
|
199
|
+
return node
|
200
|
+
end
|
201
|
+
|
202
|
+
def create_self_reference_node(atom)
|
203
|
+
node = SelfReferenceNode.new atom
|
204
|
+
@atom_nodes.push node
|
205
|
+
return node
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_reference_node(atom)
|
209
|
+
node = ReferenceNode.new atom
|
210
|
+
@atom_nodes.push node
|
211
|
+
return node
|
212
|
+
end
|
213
|
+
|
214
|
+
def create_adapter_node(side)
|
215
|
+
if side == :left
|
216
|
+
return LeftAdapterNode.new
|
217
|
+
else
|
218
|
+
return RightAdapterNode.new
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# This method is used to update each TypeNode based on the facts in
|
223
|
+
# working memory. It can be a costly operation because it iterates over
|
224
|
+
# EVERY fact in working memory. It should only be used when a new rule is
|
225
|
+
# added.
|
226
|
+
def compare_to_wm(type_node)
|
227
|
+
@working_memory.each_fact do |fact|
|
228
|
+
type_node.assert MatchContext.new(fact)
|
229
|
+
end
|
230
|
+
end
|
198
231
|
end
|
199
232
|
|
200
233
|
# Any node in the network that needs to be printed extends this class. It
|
201
|
-
# provides handles to the nodes above it in network. These are not used
|
202
|
-
# matching (i.e. no backward-chaining).
|
234
|
+
# provides handles to the nodes above it in the network. These are not used
|
235
|
+
# for matching (i.e. no backward-chaining).
|
203
236
|
class Printable
|
237
|
+
|
238
|
+
attr_reader:parent_nodes
|
239
|
+
|
204
240
|
def initialize
|
205
|
-
# this only used for printing the network, not for matching
|
241
|
+
# this is only used for printing the network, not for matching
|
206
242
|
@parent_nodes = []
|
207
243
|
end
|
208
|
-
|
244
|
+
|
209
245
|
def print(tab)
|
210
246
|
puts tab + to_s
|
211
247
|
@parent_nodes.each do |out_node|
|
@@ -216,7 +252,7 @@ module Ruleby
|
|
216
252
|
|
217
253
|
# Base Node class used by all nodes in the network that do some kind
|
218
254
|
# of matching.
|
219
|
-
class Node < Printable
|
255
|
+
class Node < Printable
|
220
256
|
|
221
257
|
# This method determines if all common tags have equal values. If any
|
222
258
|
# values are not equal then the method returns false.
|
@@ -236,96 +272,61 @@ module Ruleby
|
|
236
272
|
# other node (i.e. they are not at the bottom). It contains methods for
|
237
273
|
# propagating match results.
|
238
274
|
class ParentNode < Node
|
239
|
-
|
240
|
-
|
241
|
-
@out_nodes = []
|
242
|
-
end
|
243
|
-
attr_reader:out_nodes
|
275
|
+
|
276
|
+
attr_reader:out_nodes
|
244
277
|
|
245
|
-
def
|
246
|
-
|
247
|
-
|
278
|
+
def initialize()
|
279
|
+
super
|
280
|
+
@out_nodes = []
|
281
|
+
end
|
248
282
|
|
249
283
|
def retract(fact)
|
250
284
|
propagate_retract(fact)
|
251
|
-
end
|
285
|
+
end
|
252
286
|
|
253
|
-
def
|
287
|
+
def propagate_retract(fact)
|
254
288
|
@out_nodes.each do |out_node|
|
255
|
-
out_node.
|
289
|
+
out_node.retract(fact)
|
256
290
|
end
|
257
291
|
end
|
258
292
|
|
259
|
-
def
|
293
|
+
def assert(assertable)
|
294
|
+
propagate_assert(assertable)
|
295
|
+
end
|
296
|
+
|
297
|
+
def propagate_assert(assertable)
|
260
298
|
@out_nodes.each do |out_node|
|
261
|
-
out_node.
|
299
|
+
out_node.assert(assertable)
|
262
300
|
end
|
263
301
|
end
|
264
302
|
end
|
265
303
|
|
266
304
|
# This is a base class for all single input nodes that match facts based on
|
267
|
-
# some properties. It is essentially a wrapper for an Atom.
|
305
|
+
# some properties. It is essentially a wrapper for an Atom. These nodes make
|
306
|
+
# up the Alpha network.
|
268
307
|
class AtomNode < ParentNode
|
308
|
+
|
309
|
+
attr_reader:atom
|
310
|
+
|
269
311
|
def initialize(atom)
|
270
312
|
super()
|
271
313
|
@atom = atom
|
272
314
|
end
|
273
|
-
attr_reader:atom
|
274
|
-
|
275
|
-
def retract(fact)
|
276
|
-
# These nodes do not have a memory, so there is no need to do anything
|
277
|
-
# when a fact is retracted. Just pass it on to the two-input nodes.
|
278
|
-
propagate_retract(fact)
|
279
|
-
end
|
280
315
|
end
|
281
316
|
|
282
317
|
# This node class is used to match the type of a fact.
|
283
|
-
class TypeNode < AtomNode
|
284
|
-
def assert(
|
285
|
-
|
286
|
-
if mr.is_match
|
287
|
-
context.match.update mr
|
288
|
-
propagate_assert(context)
|
289
|
-
end
|
318
|
+
class TypeNode < AtomNode
|
319
|
+
def assert(fact)
|
320
|
+
propagate_assert(fact) if (@atom.clazz == fact.object.class)
|
290
321
|
end
|
291
|
-
|
292
|
-
def match(fact)
|
293
|
-
mr = MatchResult.new
|
294
|
-
val = fact.object.send("#{@atom.name}")
|
295
|
-
if @atom.proc.call(val)
|
296
|
-
mr.is_match = true
|
297
|
-
mr.recency.push fact.recency
|
298
|
-
mr.fact_hash[@atom.tag] = fact.id
|
299
|
-
mr[@atom.tag] = fact.object
|
300
|
-
end
|
301
|
-
return mr
|
302
|
-
end
|
303
|
-
private:match
|
304
322
|
end
|
305
323
|
|
306
324
|
# This node class is used for matching properties of a fact.
|
307
325
|
class PropertyNode < AtomNode
|
308
|
-
def assert(
|
309
|
-
mr = match(context.fact)
|
310
|
-
if mr.is_match
|
311
|
-
# TODO for node sharing will we have to update multiple matches
|
312
|
-
context.match.update mr
|
313
|
-
propagate_assert(context)
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
def match(fact)
|
318
|
-
mr = MatchResult.new
|
326
|
+
def assert(fact)
|
319
327
|
val = fact.object.send("#{@atom.name}")
|
320
|
-
if @atom.proc.call(val)
|
321
|
-
mr.is_match = true
|
322
|
-
mr.fact_hash[@atom.tag] = fact.id
|
323
|
-
mr.recency.push fact.recency
|
324
|
-
mr[@atom.tag] = val
|
325
|
-
end
|
326
|
-
return mr
|
328
|
+
propagate_assert(fact) if @atom.proc.call(val)
|
327
329
|
end
|
328
|
-
private:match
|
329
330
|
end
|
330
331
|
|
331
332
|
# This node class is used to match properties of one with the properties
|
@@ -336,8 +337,7 @@ module Ruleby
|
|
336
337
|
def match(left_context,right_fact)
|
337
338
|
val = right_fact.object.send("#{@atom.name}")
|
338
339
|
args = [val]
|
339
|
-
|
340
|
-
match = left_context.match.dup
|
340
|
+
match = left_context.match
|
341
341
|
@atom.vars.each do |var|
|
342
342
|
args.push match.variables[var]
|
343
343
|
end
|
@@ -354,16 +354,51 @@ module Ruleby
|
|
354
354
|
|
355
355
|
# This node class is used to match properties of a fact with other properties
|
356
356
|
# of itself. Unlike ReferenceAtom it does perform inline matching.
|
357
|
-
class SelfReferenceNode <
|
358
|
-
def assert(
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
357
|
+
class SelfReferenceNode < AtomNode
|
358
|
+
def assert(fact)
|
359
|
+
propagate_assert fact if match fact
|
360
|
+
end
|
361
|
+
|
362
|
+
def match(fact)
|
363
|
+
args = [fact.object.send("#{@atom.name}")]
|
364
|
+
@atom.vars.each do |var|
|
365
|
+
args.push fact.object.send(var)
|
366
|
+
end
|
367
|
+
return @atom.proc.call(*args)
|
364
368
|
end
|
365
369
|
end
|
366
370
|
|
371
|
+
# The BridgeNode is used to bridge the alpha network to either the beta
|
372
|
+
# network, or to the terminal nodes. It creates a partial match from the
|
373
|
+
# pattern and atoms above it in the network. Thus, there is one bridge node
|
374
|
+
# for each pattern (assuming they aren't shared).
|
375
|
+
class BridgeNode < ParentNode
|
376
|
+
def initialize(pattern)
|
377
|
+
super()
|
378
|
+
@pattern = pattern
|
379
|
+
end
|
380
|
+
|
381
|
+
def propagate_assert(fact)
|
382
|
+
# create the partial match
|
383
|
+
mr = MatchResult.new
|
384
|
+
mr.is_match = true
|
385
|
+
mr.recency.push fact.recency
|
386
|
+
@pattern.atoms.each do |atom|
|
387
|
+
mr.fact_hash[atom.tag] = fact.id
|
388
|
+
if atom == @pattern.head
|
389
|
+
# HACK its a pain to have to check for this, can we make it special
|
390
|
+
mr[atom.tag] = fact.object
|
391
|
+
else
|
392
|
+
mr[atom.tag] = fact.object.send("#{atom.name}")
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
context = MatchContext.new(fact,mr)
|
397
|
+
|
398
|
+
super(context)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
367
402
|
# This class is used to plug nodes into the left input of a two-input JoinNode
|
368
403
|
class LeftAdapterNode < ParentNode
|
369
404
|
def propagate_assert(context)
|
@@ -379,8 +414,9 @@ module Ruleby
|
|
379
414
|
end
|
380
415
|
end
|
381
416
|
|
382
|
-
# This class is used to plug nodes into the right input of a two-input
|
383
|
-
|
417
|
+
# This class is used to plug nodes into the right input of a two-input
|
418
|
+
# JoinNode
|
419
|
+
class RightAdapterNode < ParentNode
|
384
420
|
def propagate_assert(context)
|
385
421
|
@out_nodes.each do |out_node|
|
386
422
|
out_node.assert_right(context)
|
@@ -396,15 +432,18 @@ module Ruleby
|
|
396
432
|
|
397
433
|
# This class is a two-input node that is used to create a cross-product of the
|
398
434
|
# two network branches above it. It keeps a memory of the left and right
|
399
|
-
# inputs and compares new facts to each.
|
435
|
+
# inputs and compares new facts to each. These nodes make up what is called
|
436
|
+
# the Beta network.
|
400
437
|
class JoinNode < ParentNode
|
438
|
+
|
439
|
+
attr:ref_nodes,true
|
440
|
+
|
401
441
|
def initialize
|
402
442
|
super
|
403
443
|
@left_memory = {}
|
404
444
|
@right_memory = {}
|
405
445
|
@ref_nodes = []
|
406
446
|
end
|
407
|
-
attr:ref_nodes,true
|
408
447
|
|
409
448
|
def retract_left(fact)
|
410
449
|
@left_memory.delete(fact.id)
|
@@ -421,7 +460,6 @@ module Ruleby
|
|
421
460
|
@right_memory.values.each do |right_context|
|
422
461
|
mr = match_ref_nodes(context,right_context)
|
423
462
|
if (mr.is_match)
|
424
|
-
# TODO for node sharing we will have to update multiple matches
|
425
463
|
new_context = MatchContext.new context.fact, mr
|
426
464
|
propagate_assert(new_context)
|
427
465
|
end
|
@@ -433,62 +471,61 @@ module Ruleby
|
|
433
471
|
@left_memory.values.flatten.each do |left_context|
|
434
472
|
mr = match_ref_nodes(left_context,context)
|
435
473
|
if (mr.is_match)
|
436
|
-
|
437
|
-
new_context = MatchContext.new context.fact, mr
|
474
|
+
new_context = MatchContext.new context.fact, mr
|
438
475
|
propagate_assert(new_context)
|
439
476
|
end
|
440
477
|
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def to_s
|
481
|
+
return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
|
441
482
|
end
|
442
483
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
484
|
+
private
|
485
|
+
def match_ref_nodes(left_context,right_context)
|
486
|
+
mr = right_context.match
|
487
|
+
if @ref_nodes.empty?
|
488
|
+
return left_context.match.merge(mr)
|
489
|
+
else
|
490
|
+
@ref_nodes.each do |ref_node|
|
491
|
+
ref_mr = ref_node.match(left_context, right_context.fact)
|
492
|
+
if ref_mr.is_match
|
493
|
+
mr = mr.merge ref_mr
|
494
|
+
else
|
495
|
+
return MatchResult.new
|
496
|
+
end
|
455
497
|
end
|
498
|
+
return mr
|
456
499
|
end
|
457
|
-
return mr
|
458
500
|
end
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
# being added to the left_memory. Double check that this is not happening
|
469
|
-
end
|
470
|
-
|
471
|
-
def propagate_retract_resolve(match)
|
472
|
-
@out_nodes.each do |o|
|
473
|
-
o.retract_resolve(match)
|
501
|
+
|
502
|
+
def add_to_left_memory(context)
|
503
|
+
lm = @left_memory[context.fact.id]
|
504
|
+
lm = [] unless lm
|
505
|
+
lm.push context
|
506
|
+
@left_memory[context.fact.id] = lm
|
507
|
+
# QUESTION for a little while we were having trouble with duplicate
|
508
|
+
# contexts being added to the left_memory. Double check that this is
|
509
|
+
# not happening
|
474
510
|
end
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
511
|
+
|
512
|
+
def propagate_retract_resolve(match)
|
513
|
+
@out_nodes.each do |o|
|
514
|
+
o.retract_resolve(match)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
def retract_resolve(match)
|
519
|
+
# in this method we retract an existing match from memory if it resolves
|
520
|
+
# with the match given. It would probably be better to check if it
|
521
|
+
# resolves with a list of facts. But the system is not set up for
|
522
|
+
# that yet.
|
523
|
+
@left_memory.each do |fact_id,contexts|
|
524
|
+
value.delete_if do |left_context|
|
525
|
+
resolve(left_context.match, match)
|
526
|
+
end
|
527
|
+
end
|
486
528
|
end
|
487
|
-
end
|
488
|
-
|
489
|
-
def to_s
|
490
|
-
return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
|
491
|
-
end
|
492
529
|
end
|
493
530
|
|
494
531
|
# This node class is used when a rule is looking for a fact that does not
|
@@ -509,13 +546,15 @@ module Ruleby
|
|
509
546
|
right_context = @right_memory.delete(fact.id)
|
510
547
|
unless right_context == @right_memory.default
|
511
548
|
unless @ref_nodes.empty? && !@right_memory.empty?
|
512
|
-
@left_memory.values.each do |
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
549
|
+
@left_memory.values.each do |lm|
|
550
|
+
lm.each do |left_context|
|
551
|
+
# TODO we should cache the matches on the left that were unmatched
|
552
|
+
# by a result from a NotPattern. We could hash them by the right
|
553
|
+
# match that caused this. That we way we would not have to
|
554
|
+
# re-compare the the left and right matches.
|
555
|
+
if match_ref_nodes(left_context,right_context)
|
556
|
+
propagate_assert(left_context)
|
557
|
+
end
|
519
558
|
end
|
520
559
|
end
|
521
560
|
end
|
@@ -562,10 +601,12 @@ module Ruleby
|
|
562
601
|
end
|
563
602
|
|
564
603
|
# This class represents the bottom node in the network. There is a one to one
|
565
|
-
# relation
|
604
|
+
# relation between TerminalNodes and Rules. A terminal node acts as a wrapper
|
566
605
|
# for a rule. The class is responsible for keeping a memory of the
|
567
606
|
# activations that have been generated by the network.
|
568
607
|
class TerminalNode < Node
|
608
|
+
@@counter = 0
|
609
|
+
|
569
610
|
def initialize(rule)
|
570
611
|
super()
|
571
612
|
@rule = rule
|
@@ -574,7 +615,9 @@ module Ruleby
|
|
574
615
|
attr_reader:activations
|
575
616
|
|
576
617
|
def assert(context)
|
577
|
-
|
618
|
+
match = context.match
|
619
|
+
a = Activation.new(@rule.action, match, @@counter)
|
620
|
+
@activations.add match.fact_ids, a
|
578
621
|
end
|
579
622
|
|
580
623
|
def retract(fact)
|
@@ -598,24 +641,13 @@ module Ruleby
|
|
598
641
|
resolve(activation.match, match)
|
599
642
|
end
|
600
643
|
end
|
601
|
-
end
|
602
|
-
|
603
|
-
class MatchContext
|
604
|
-
def initialize(fact,mr=MatchResult.new)
|
605
|
-
@fact = fact
|
606
|
-
|
607
|
-
# TODO for node sharing this will be a pattern=>match Hash
|
608
|
-
@match = mr
|
609
|
-
end
|
610
|
-
attr_reader:fact
|
611
|
-
attr_reader:match
|
612
644
|
|
613
|
-
def
|
614
|
-
|
645
|
+
def self.increment_counter
|
646
|
+
@@counter = @@counter + 1
|
615
647
|
end
|
616
648
|
|
617
|
-
def
|
618
|
-
|
649
|
+
def self.reset_counter
|
650
|
+
@@counter = 0
|
619
651
|
end
|
620
652
|
end
|
621
653
|
|