ruleby 0.7 → 0.8.b1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/core/atoms.rb +2 -2
- data/lib/core/engine.rb +272 -268
- data/lib/core/nodes.rb +95 -7
- data/lib/core/patterns.rb +118 -115
- data/lib/dsl/ferrari.rb +45 -30
- data/lib/dsl/letigre.rb +1 -1
- metadata +37 -26
data/lib/core/atoms.rb
CHANGED
@@ -136,7 +136,7 @@ module Ruleby
|
|
136
136
|
# attribute represents a Class type, and the mode defines whether the head
|
137
137
|
# will match only class that are exactly a particular type, or if it will
|
138
138
|
# match classes that inherit that type also.
|
139
|
-
class
|
139
|
+
class Template
|
140
140
|
attr_reader :clazz
|
141
141
|
attr_reader :mode
|
142
142
|
|
@@ -146,7 +146,7 @@ module Ruleby
|
|
146
146
|
end
|
147
147
|
|
148
148
|
def ==(df)
|
149
|
-
|
149
|
+
Template === df && df.clazz == @clazz && df.mode == @mode
|
150
150
|
end
|
151
151
|
end
|
152
152
|
|
data/lib/core/engine.rb
CHANGED
@@ -1,269 +1,273 @@
|
|
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, Matt Smith
|
10
|
-
#
|
11
|
-
|
12
|
-
require 'core/atoms'
|
13
|
-
require 'core/patterns'
|
14
|
-
require 'core/utils'
|
15
|
-
require 'core/nodes'
|
16
|
-
|
17
|
-
module Ruleby
|
18
|
-
module Core
|
19
|
-
|
20
|
-
# An action is a wrapper for a code block that will be executed if a rule is
|
21
|
-
# satisfied.
|
22
|
-
class Action
|
23
|
-
attr_accessor :priority
|
24
|
-
attr_accessor :name
|
25
|
-
attr_reader :matches
|
26
|
-
attr_reader :proc
|
27
|
-
|
28
|
-
def initialize(&block)
|
29
|
-
@name = nil
|
30
|
-
@proc = Proc.new(&block) if block_given?
|
31
|
-
@priority = 0
|
32
|
-
end
|
33
|
-
|
34
|
-
def fire(match, engine=nil)
|
35
|
-
if @proc.arity == 2
|
36
|
-
@proc.call(match, engine)
|
37
|
-
else
|
38
|
-
@proc.call(match)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def ==(a2)
|
43
|
-
return @name == a2.name
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# An activation is an action/match pair that is executed if a rule is matched.
|
48
|
-
# It also contains metadata that can be used for conflict resolution if two
|
49
|
-
# rules are satisfied by the same fact.
|
50
|
-
class Activation
|
51
|
-
attr_reader :action, :match
|
52
|
-
attr_accessor :counter, :used
|
53
|
-
|
54
|
-
def initialize(action, match, counter=0)
|
55
|
-
@action = action
|
56
|
-
@match = match
|
57
|
-
@match.recency.sort!
|
58
|
-
@match.recency.reverse!
|
59
|
-
@counter = counter
|
60
|
-
@used = false
|
61
|
-
end
|
62
|
-
|
63
|
-
def fire(engine=nil)
|
64
|
-
@used = true
|
65
|
-
@action.fire @match, engine
|
66
|
-
end
|
67
|
-
|
68
|
-
def <=>(a2)
|
69
|
-
return @counter <=> a2.counter if @counter != a2.counter
|
70
|
-
return @action.priority <=> a2.action.priority if @action.priority != a2.action.priority
|
71
|
-
|
72
|
-
# NOTE in order for this to work, the array must be reverse sorted
|
73
|
-
i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1
|
74
|
-
i += 1
|
75
|
-
end
|
76
|
-
return @match.recency[i] <=> a2.match.recency[i]
|
77
|
-
end
|
78
|
-
|
79
|
-
def ==(a2)
|
80
|
-
return a2 != nil && @action == a2.action && @match == a2.match
|
81
|
-
end
|
82
|
-
|
83
|
-
def to_s
|
84
|
-
return "[#{@action.name}-#{object_id}|#{@counter}|#{@action.priority}|#{@match.recency.join(',')}|#{@match.to_s}] "
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
class Rule
|
89
|
-
attr_accessor :pattern
|
90
|
-
attr_reader :action, :name, :priority
|
91
|
-
|
92
|
-
def initialize(name, pattern=nil, action=nil, priority=0)
|
93
|
-
@name = name
|
94
|
-
@pattern = pattern
|
95
|
-
@action = action
|
96
|
-
@priority = priority
|
97
|
-
end
|
98
|
-
|
99
|
-
def priority=(p)
|
100
|
-
@priority = p
|
101
|
-
@action.priority = @priority
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
# A fact is an object that is stored in working memory. The rules in the
|
106
|
-
# system will either look for the existence or absence of particular facts.
|
107
|
-
class Fact
|
108
|
-
attr :token, true
|
109
|
-
attr :recency, true
|
110
|
-
attr_reader :object
|
111
|
-
|
112
|
-
def initialize(object, token)
|
113
|
-
@token = token
|
114
|
-
@object = object
|
115
|
-
end
|
116
|
-
|
117
|
-
def id
|
118
|
-
return object.object_id
|
119
|
-
end
|
120
|
-
|
121
|
-
def ==(fact)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
@facts
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
end
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
end
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
@root
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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, Matt Smith
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'core/atoms'
|
13
|
+
require 'core/patterns'
|
14
|
+
require 'core/utils'
|
15
|
+
require 'core/nodes'
|
16
|
+
|
17
|
+
module Ruleby
|
18
|
+
module Core
|
19
|
+
|
20
|
+
# An action is a wrapper for a code block that will be executed if a rule is
|
21
|
+
# satisfied.
|
22
|
+
class Action
|
23
|
+
attr_accessor :priority
|
24
|
+
attr_accessor :name
|
25
|
+
attr_reader :matches
|
26
|
+
attr_reader :proc
|
27
|
+
|
28
|
+
def initialize(&block)
|
29
|
+
@name = nil
|
30
|
+
@proc = Proc.new(&block) if block_given?
|
31
|
+
@priority = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def fire(match, engine=nil)
|
35
|
+
if @proc.arity == 2
|
36
|
+
@proc.call(match, engine)
|
37
|
+
else
|
38
|
+
@proc.call(match)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def ==(a2)
|
43
|
+
return @name == a2.name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# An activation is an action/match pair that is executed if a rule is matched.
|
48
|
+
# It also contains metadata that can be used for conflict resolution if two
|
49
|
+
# rules are satisfied by the same fact.
|
50
|
+
class Activation
|
51
|
+
attr_reader :action, :match
|
52
|
+
attr_accessor :counter, :used
|
53
|
+
|
54
|
+
def initialize(action, match, counter=0)
|
55
|
+
@action = action
|
56
|
+
@match = match
|
57
|
+
@match.recency.sort!
|
58
|
+
@match.recency.reverse!
|
59
|
+
@counter = counter
|
60
|
+
@used = false
|
61
|
+
end
|
62
|
+
|
63
|
+
def fire(engine=nil)
|
64
|
+
@used = true
|
65
|
+
@action.fire @match, engine
|
66
|
+
end
|
67
|
+
|
68
|
+
def <=>(a2)
|
69
|
+
return @counter <=> a2.counter if @counter != a2.counter
|
70
|
+
return @action.priority <=> a2.action.priority if @action.priority != a2.action.priority
|
71
|
+
|
72
|
+
# NOTE in order for this to work, the array must be reverse sorted
|
73
|
+
i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1
|
74
|
+
i += 1
|
75
|
+
end
|
76
|
+
return @match.recency[i] <=> a2.match.recency[i]
|
77
|
+
end
|
78
|
+
|
79
|
+
def ==(a2)
|
80
|
+
return a2 != nil && @action == a2.action && @match == a2.match
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
return "[#{@action.name}-#{object_id}|#{@counter}|#{@action.priority}|#{@match.recency.join(',')}|#{@match.to_s}] "
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Rule
|
89
|
+
attr_accessor :pattern
|
90
|
+
attr_reader :action, :name, :priority
|
91
|
+
|
92
|
+
def initialize(name, pattern=nil, action=nil, priority=0)
|
93
|
+
@name = name
|
94
|
+
@pattern = pattern
|
95
|
+
@action = action
|
96
|
+
@priority = priority
|
97
|
+
end
|
98
|
+
|
99
|
+
def priority=(p)
|
100
|
+
@priority = p
|
101
|
+
@action.priority = @priority
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# A fact is an object that is stored in working memory. The rules in the
|
106
|
+
# system will either look for the existence or absence of particular facts.
|
107
|
+
class Fact
|
108
|
+
attr :token, true
|
109
|
+
attr :recency, true
|
110
|
+
attr_reader :object
|
111
|
+
|
112
|
+
def initialize(object, token)
|
113
|
+
@token = token
|
114
|
+
@object = object
|
115
|
+
end
|
116
|
+
|
117
|
+
def id
|
118
|
+
return object.object_id
|
119
|
+
end
|
120
|
+
|
121
|
+
def ==(fact)
|
122
|
+
if fact.is_a? Fact
|
123
|
+
fact != nil && fact.id == id
|
124
|
+
else
|
125
|
+
fact != nil && fact.object_id == id
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_s
|
130
|
+
"[Fact |#{@recency}|#{@object.to_s}]"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# A conflict resolver is used to order activations that become active at the
|
135
|
+
# same time. The default implementation sorts the agenda based on the
|
136
|
+
# properties of the activation.
|
137
|
+
class RulebyConflictResolver
|
138
|
+
def resolve(agenda)
|
139
|
+
return agenda.sort
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# The working memory is a container for all the facts in the system. The
|
144
|
+
# inference engine will compare these facts with the rules to produce some
|
145
|
+
# outcomes.
|
146
|
+
class WorkingMemory
|
147
|
+
attr_reader :facts
|
148
|
+
|
149
|
+
def initialize
|
150
|
+
@recency = 0
|
151
|
+
@facts = Array.new
|
152
|
+
end
|
153
|
+
|
154
|
+
def each_fact
|
155
|
+
@facts.each do |f|
|
156
|
+
yield(f)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def assert_fact(fact)
|
161
|
+
raise 'The fact asserted cannot be nil!' unless fact.object
|
162
|
+
if (fact.token == :plus)
|
163
|
+
fact.recency = @recency
|
164
|
+
@recency += 1
|
165
|
+
@facts.push fact
|
166
|
+
return fact
|
167
|
+
else #if (fact.token == :minus)
|
168
|
+
i = @facts.index(fact)
|
169
|
+
raise 'The fact to remove does not exist!' unless i
|
170
|
+
existing_fact = @facts[i]
|
171
|
+
@facts.delete_at(i)
|
172
|
+
existing_fact.token = fact.token
|
173
|
+
return existing_fact
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def print
|
178
|
+
puts 'WORKING MEMORY:'
|
179
|
+
@facts.each do |fact|
|
180
|
+
puts " #{fact.object} - #{fact.id} - #{fact.recency}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# This is the core class of the library. A new rule engine is created by
|
186
|
+
# instantiating it. Each rule engine has one inference engine, one rule set
|
187
|
+
# and one working memory.
|
188
|
+
class Engine
|
189
|
+
|
190
|
+
def initialize(wm=WorkingMemory.new,cr=RulebyConflictResolver.new)
|
191
|
+
@root = nil
|
192
|
+
@working_memory = wm
|
193
|
+
@conflict_resolver = cr
|
194
|
+
@wm_altered = false
|
195
|
+
assert InitialFact.new
|
196
|
+
end
|
197
|
+
|
198
|
+
def facts
|
199
|
+
@working_memory.facts.collect{|f| f.object}
|
200
|
+
end
|
201
|
+
|
202
|
+
# This method id called to add a new fact to working memory
|
203
|
+
def assert(object,&block)
|
204
|
+
@wm_altered = true
|
205
|
+
fact_helper(object,:plus,&block)
|
206
|
+
end
|
207
|
+
|
208
|
+
# This method is called to remove an existing fact from working memory
|
209
|
+
def retract(object,&block)
|
210
|
+
@wm_altered = true
|
211
|
+
fact_helper(object,:minus,&block)
|
212
|
+
end
|
213
|
+
|
214
|
+
# This method is called to alter an existing fact. It is essentially a
|
215
|
+
# retract followed by an assert.
|
216
|
+
def modify(object,&block)
|
217
|
+
retract(object,&block)
|
218
|
+
assert(object,&block)
|
219
|
+
end
|
220
|
+
|
221
|
+
def retrieve(c)
|
222
|
+
facts.select {|f| f.kind_of?(c)}
|
223
|
+
end
|
224
|
+
|
225
|
+
# This method adds a new rule to the system.
|
226
|
+
def assert_rule(rule)
|
227
|
+
if @root == nil
|
228
|
+
@root = RootNode.new(@working_memory)
|
229
|
+
@root.reset_counter
|
230
|
+
end
|
231
|
+
@root.assert_rule rule
|
232
|
+
end
|
233
|
+
|
234
|
+
# This method executes the activations that were generated by the rules
|
235
|
+
# that match facts in working memory.
|
236
|
+
def match(agenda=nil, used_agenda=[])
|
237
|
+
if @root
|
238
|
+
@root.reset_counter
|
239
|
+
agenda = @root.matches unless agenda
|
240
|
+
while (agenda.length > 0)
|
241
|
+
agenda = @conflict_resolver.resolve agenda
|
242
|
+
activation = agenda.pop
|
243
|
+
used_agenda.push activation
|
244
|
+
activation.fire self
|
245
|
+
if @wm_altered
|
246
|
+
agenda = @root.matches(false)
|
247
|
+
@root.increment_counter
|
248
|
+
@wm_altered = false
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def print
|
255
|
+
@working_memory.print
|
256
|
+
@root.print
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
def fact_helper(object, sign=:plus, &block)
|
261
|
+
f = Core::Fact.new object, sign
|
262
|
+
yield f if block_given?
|
263
|
+
assert_fact f
|
264
|
+
f
|
265
|
+
end
|
266
|
+
|
267
|
+
def assert_fact(fact)
|
268
|
+
wm_fact = @working_memory.assert_fact fact
|
269
|
+
@root.assert_fact wm_fact if @root != nil
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
269
273
|
end
|
data/lib/core/nodes.rb
CHANGED
@@ -200,7 +200,11 @@ module Ruleby
|
|
200
200
|
end
|
201
201
|
|
202
202
|
def create_bridge_node(pattern)
|
203
|
-
|
203
|
+
if pattern.kind_of?(CollectPattern)
|
204
|
+
CollectNode.new(pattern)
|
205
|
+
else
|
206
|
+
BridgeNode.new(pattern)
|
207
|
+
end
|
204
208
|
end
|
205
209
|
|
206
210
|
def create_property_node(atom,forked)
|
@@ -483,12 +487,14 @@ module Ruleby
|
|
483
487
|
# network, or to the terminal nodes. It creates a partial match from the
|
484
488
|
# pattern and atoms above it in the network. Thus, there is one bridge node
|
485
489
|
# for each pattern (assuming they aren't shared).
|
486
|
-
class
|
490
|
+
class BaseBridgeNode < ParentNode
|
487
491
|
def initialize(pattern)
|
488
492
|
super()
|
489
493
|
@pattern = pattern
|
490
494
|
end
|
491
|
-
|
495
|
+
end
|
496
|
+
|
497
|
+
class BridgeNode < BaseBridgeNode
|
492
498
|
def propagate_assert(fact)
|
493
499
|
# create the partial match
|
494
500
|
mr = MatchResult.new
|
@@ -505,7 +511,72 @@ module Ruleby
|
|
505
511
|
end
|
506
512
|
super(MatchContext.new(fact,mr))
|
507
513
|
end
|
508
|
-
end
|
514
|
+
end
|
515
|
+
|
516
|
+
class CollectNode < BaseBridgeNode
|
517
|
+
def initialize(pattern)
|
518
|
+
super
|
519
|
+
@collection_memory = Fact.new([], :internal)
|
520
|
+
@should_modify = false
|
521
|
+
# not really sure what to do about this. might just need to handle nil's
|
522
|
+
# using a puts a limit on how many facts can be asserted before this feature breaks
|
523
|
+
# it might be best to get a handle to WorkingMemory in order to get the real
|
524
|
+
# @collection_memory.recency = 0
|
525
|
+
end
|
526
|
+
|
527
|
+
def retract(fact)
|
528
|
+
propagate_retract(@collection_memory)
|
529
|
+
propagate_assert(fact) do
|
530
|
+
@collection_memory.object.delete_if {|a| a == fact.object}
|
531
|
+
@should_modify = false
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def propagate_modify(context, out_nodes=@out_nodes)
|
536
|
+
out_nodes.each do |out_node|
|
537
|
+
if out_node.is_a?(TerminalNode)
|
538
|
+
out_node.modify(context)
|
539
|
+
else
|
540
|
+
raise "You can't join to :collect patterns yet!"
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def propagate_assert(fact)
|
546
|
+
if block_given?
|
547
|
+
yield fact
|
548
|
+
else
|
549
|
+
@collection_memory.object << fact
|
550
|
+
@collection_memory.recency = fact.recency
|
551
|
+
end
|
552
|
+
|
553
|
+
if @should_modify
|
554
|
+
propagate_modify(MatchContext.new(@collection_memory, create_match_result))
|
555
|
+
else
|
556
|
+
if @collection_memory.object.size > 0
|
557
|
+
@should_modify = true
|
558
|
+
super(MatchContext.new(@collection_memory, create_match_result))
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
private
|
564
|
+
|
565
|
+
def create_match_result()
|
566
|
+
# create the partial match
|
567
|
+
mr = MatchResult.new
|
568
|
+
mr.is_match = true
|
569
|
+
mr.recency.push @collection_memory.recency
|
570
|
+
|
571
|
+
@pattern.atoms.each do |atom|
|
572
|
+
mr.fact_hash[atom.tag] = @collection_memory.id
|
573
|
+
if atom == @pattern.head
|
574
|
+
mr[atom.tag] = @collection_memory.object
|
575
|
+
end
|
576
|
+
end
|
577
|
+
mr
|
578
|
+
end
|
579
|
+
end
|
509
580
|
|
510
581
|
# This class is used to plug nodes into the left input of a two-input JoinNode
|
511
582
|
class LeftAdapterNode < ParentNode
|
@@ -549,7 +620,7 @@ module Ruleby
|
|
549
620
|
end
|
550
621
|
end
|
551
622
|
end
|
552
|
-
|
623
|
+
|
553
624
|
# This class is a two-input node that is used to create a cross-product of the
|
554
625
|
# two network branches above it. It keeps a memory of the left and right
|
555
626
|
# inputs and compares new facts to each. These nodes make up what is called
|
@@ -740,13 +811,30 @@ module Ruleby
|
|
740
811
|
@rule = rule
|
741
812
|
@activations = MultiHash.new
|
742
813
|
end
|
743
|
-
attr_reader:activations
|
744
|
-
|
814
|
+
#attr_reader:activations
|
815
|
+
|
816
|
+
def activations
|
817
|
+
@activations
|
818
|
+
end
|
819
|
+
|
745
820
|
def assert(context)
|
746
821
|
match = context.match
|
747
822
|
a = Activation.new(@rule.action, match, @@counter)
|
748
823
|
@activations.add match.fact_ids, a
|
749
824
|
end
|
825
|
+
|
826
|
+
def modify(context)
|
827
|
+
found = false
|
828
|
+
@activations.each do |id, v|
|
829
|
+
if context.match.fact_ids.sort == id.sort
|
830
|
+
v.replace(context.fact)
|
831
|
+
found = true
|
832
|
+
end
|
833
|
+
end
|
834
|
+
if !found
|
835
|
+
assert(context)
|
836
|
+
end
|
837
|
+
end
|
750
838
|
|
751
839
|
def retract(fact)
|
752
840
|
@activations.remove fact.id
|
data/lib/core/patterns.rb
CHANGED
@@ -1,116 +1,119 @@
|
|
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
|
-
|
12
|
-
module Ruleby
|
13
|
-
module Core
|
14
|
-
|
15
|
-
class Pattern
|
16
|
-
end
|
17
|
-
|
18
|
-
# This class represents a pattern that is looking for the existence of some
|
19
|
-
# object. It contains a list of 'atoms' that represent the properties of
|
20
|
-
# the class that we are looking for.
|
21
|
-
class ObjectPattern < Pattern
|
22
|
-
attr_reader :atoms
|
23
|
-
|
24
|
-
def initialize(head, atoms)
|
25
|
-
@atoms = [head] + atoms
|
26
|
-
end
|
27
|
-
|
28
|
-
def head
|
29
|
-
@atoms[0]
|
30
|
-
end
|
31
|
-
|
32
|
-
def ==(pattern)
|
33
|
-
if pattern.class == self.class
|
34
|
-
atoms = pattern.atoms
|
35
|
-
if(@atoms.size == atoms.size)
|
36
|
-
(0..@atoms.size).each do |i|
|
37
|
-
if !(@atoms[i] == atoms[i])
|
38
|
-
return false
|
39
|
-
end
|
40
|
-
end
|
41
|
-
return true
|
42
|
-
end
|
43
|
-
end
|
44
|
-
return false
|
45
|
-
end
|
46
|
-
|
47
|
-
def to_s
|
48
|
-
return '(' + @atoms.join('|') + ')'
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class InheritsPattern < ObjectPattern
|
53
|
-
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
class
|
59
|
-
|
60
|
-
|
61
|
-
class
|
62
|
-
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
+
|
12
|
+
module Ruleby
|
13
|
+
module Core
|
14
|
+
|
15
|
+
class Pattern
|
16
|
+
end
|
17
|
+
|
18
|
+
# This class represents a pattern that is looking for the existence of some
|
19
|
+
# object. It contains a list of 'atoms' that represent the properties of
|
20
|
+
# the class that we are looking for.
|
21
|
+
class ObjectPattern < Pattern
|
22
|
+
attr_reader :atoms
|
23
|
+
|
24
|
+
def initialize(head, atoms)
|
25
|
+
@atoms = [head] + atoms
|
26
|
+
end
|
27
|
+
|
28
|
+
def head
|
29
|
+
@atoms[0]
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(pattern)
|
33
|
+
if pattern.class == self.class
|
34
|
+
atoms = pattern.atoms
|
35
|
+
if(@atoms.size == atoms.size)
|
36
|
+
(0..@atoms.size).each do |i|
|
37
|
+
if !(@atoms[i] == atoms[i])
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
return '(' + @atoms.join('|') + ')'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class InheritsPattern < ObjectPattern
|
53
|
+
end
|
54
|
+
|
55
|
+
class CollectPattern < ObjectPattern
|
56
|
+
end
|
57
|
+
|
58
|
+
# This class represents a pattern that is looking for the absence of some
|
59
|
+
# object (rather than the existence of). In all respects, it is the same as
|
60
|
+
# an ObjectPattern, but it is handled differently by the inference engine.
|
61
|
+
class NotPattern < ObjectPattern
|
62
|
+
end
|
63
|
+
|
64
|
+
class NotInheritsPattern < InheritsPattern
|
65
|
+
end
|
66
|
+
|
67
|
+
# A composite pattern represents a logical conjunction of two patterns. The
|
68
|
+
# inference engine interprets this differently from an ObjectPattern because
|
69
|
+
# it simply aggregates patterns.
|
70
|
+
class CompositePattern < Pattern
|
71
|
+
|
72
|
+
attr_reader :left_pattern
|
73
|
+
attr_reader :right_pattern
|
74
|
+
|
75
|
+
def initialize(left_pattern, right_pattern)
|
76
|
+
@left_pattern = left_pattern
|
77
|
+
@right_pattern = right_pattern
|
78
|
+
end
|
79
|
+
|
80
|
+
def atoms
|
81
|
+
atoms = []
|
82
|
+
atoms.push @left_pattern.atoms
|
83
|
+
atoms.push @right_pattern.atoms
|
84
|
+
return atoms
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class AndPattern < CompositePattern
|
89
|
+
|
90
|
+
def initialize(left_pattern, right_pattern)
|
91
|
+
super(left_pattern, right_pattern)
|
92
|
+
@head = :and
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class OrPattern < CompositePattern
|
98
|
+
|
99
|
+
def initialize(left_pattern, right_pattern)
|
100
|
+
super(left_pattern, right_pattern)
|
101
|
+
@head = :or
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
class InitialFactPattern < ObjectPattern
|
107
|
+
def initialize
|
108
|
+
deftemplate = Template.new InitialFact, :equals
|
109
|
+
htag = GeneratedTag.new
|
110
|
+
head = HeadAtom.new htag, deftemplate
|
111
|
+
super(head, [])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class PatternFactory
|
116
|
+
# TODO add some convenience methods for creating patterns
|
117
|
+
end
|
118
|
+
end
|
116
119
|
end
|
data/lib/dsl/ferrari.rb
CHANGED
@@ -216,12 +216,15 @@ module Ruleby
|
|
216
216
|
def when(*args)
|
217
217
|
clazz = AtomBuilder === args[0] ? nil : args.shift
|
218
218
|
is_not = false
|
219
|
+
is_collect = false
|
219
220
|
mode = :equals
|
220
221
|
while clazz.is_a? Symbol
|
221
222
|
if clazz == :not || clazz == :~
|
222
223
|
is_not = true
|
223
224
|
elsif clazz == :is_a? || clazz == :kind_of? || clazz == :instance_of?
|
224
225
|
mode = :inherits
|
226
|
+
elsif clazz == :collect
|
227
|
+
is_collect = true
|
225
228
|
elsif clazz == :exists?
|
226
229
|
raise 'The \'exists\' quantifier is not yet supported.'
|
227
230
|
end
|
@@ -233,7 +236,7 @@ module Ruleby
|
|
233
236
|
mode = :inherits
|
234
237
|
end
|
235
238
|
|
236
|
-
deftemplate = Core::
|
239
|
+
deftemplate = Core::Template.new clazz, mode
|
237
240
|
atoms = []
|
238
241
|
@when_counter += 1
|
239
242
|
htag = Symbol === args[0] ? args.shift : GeneratedTag.new
|
@@ -259,14 +262,15 @@ module Ruleby
|
|
259
262
|
else
|
260
263
|
raise "Invalid condition: #{arg}"
|
261
264
|
end
|
262
|
-
end
|
265
|
+
end
|
263
266
|
|
264
267
|
if is_not
|
265
268
|
p = mode==:inherits ? Core::NotInheritsPattern.new(head, atoms) :
|
266
269
|
Core::NotPattern.new(head, atoms)
|
267
270
|
else
|
268
271
|
p = mode==:inherits ? Core::InheritsPattern.new(head, atoms) :
|
269
|
-
Core::
|
272
|
+
is_collect ? Core::CollectPattern.new(head, atoms) :
|
273
|
+
Core::ObjectPattern.new(head, atoms)
|
270
274
|
end
|
271
275
|
@pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p
|
272
276
|
end
|
@@ -293,6 +297,8 @@ module Ruleby
|
|
293
297
|
|
294
298
|
class MethodBuilder
|
295
299
|
public_instance_methods.each do |m|
|
300
|
+
# maybe we shouldn't be undefing object_id. What are the implications? Can we make object_id a
|
301
|
+
# pass through to the underlying object's object_id?
|
296
302
|
a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__]
|
297
303
|
undef_method m.to_sym unless a.include? m.to_sym
|
298
304
|
end
|
@@ -318,31 +324,31 @@ module Ruleby
|
|
318
324
|
@tag = tag
|
319
325
|
@method = method
|
320
326
|
end
|
321
|
-
|
327
|
+
|
322
328
|
def +(arg)
|
323
329
|
raise 'Cannot use operators in short-hand mode!'
|
324
330
|
end
|
325
|
-
|
331
|
+
|
326
332
|
def -(arg)
|
327
333
|
raise 'Cannot use operators in short-hand mode!'
|
328
334
|
end
|
329
|
-
|
335
|
+
|
330
336
|
def /(arg)
|
331
337
|
raise 'Cannot use operators in short-hand mode!'
|
332
338
|
end
|
333
|
-
|
339
|
+
|
334
340
|
def *(arg)
|
335
341
|
raise 'Cannot use operators in short-hand mode!'
|
336
342
|
end
|
337
|
-
|
343
|
+
|
338
344
|
def to_s
|
339
345
|
"BindingBuilder @tag=#{@tag}, @method=#{@method}"
|
340
346
|
end
|
341
347
|
end
|
342
|
-
|
348
|
+
|
343
349
|
class AtomBuilder
|
344
350
|
attr_accessor :tag, :name, :bindings, :deftemplate, :block
|
345
|
-
|
351
|
+
|
346
352
|
def initialize(method_id)
|
347
353
|
@name = method_id
|
348
354
|
@deftemplate = nil
|
@@ -351,39 +357,48 @@ module Ruleby
|
|
351
357
|
@block = lambda {|x| true}
|
352
358
|
@child_atom_builders = []
|
353
359
|
end
|
354
|
-
|
360
|
+
|
355
361
|
def method_missing(method_id, *args, &block)
|
356
362
|
if method_id == :not
|
357
363
|
return NotOperatorBuilder.new(@name)
|
358
364
|
end
|
359
365
|
end
|
360
|
-
|
366
|
+
|
361
367
|
def ==(value)
|
362
368
|
@atom_type = :equals
|
363
369
|
@value = value
|
364
|
-
create_block value, lambda {|x,y| x == y}, lambda {|x| x == value}; self
|
370
|
+
create_block value, lambda {|x,y| convert(x, y) == y}, lambda {|x| convert(x, value) == value}; self
|
365
371
|
end
|
366
|
-
|
372
|
+
|
367
373
|
def >(value)
|
368
|
-
create_block value, lambda {|x,y| x > y}, lambda {|x| x > value}; self
|
374
|
+
create_block value, lambda {|x,y| x != nil && convert(x, y) > y}, lambda {|x| x != nil && convert(x, value) > value}; self
|
369
375
|
end
|
370
|
-
|
376
|
+
|
371
377
|
def <(value)
|
372
|
-
create_block value, lambda {|x,y| x < y}, lambda {|x| x < value}; self
|
378
|
+
create_block value, lambda {|x,y| x != nil && convert(x, y) < y}, lambda {|x| x != nil && convert(x, value) < value}; self
|
373
379
|
end
|
374
|
-
|
380
|
+
|
375
381
|
def =~(value)
|
376
|
-
create_block value, lambda {|x,y| x =~ y}, lambda {|x| x =~ value}; self
|
382
|
+
create_block value, lambda {|x,y| convert(x, y) =~ y}, lambda {|x| convert(x, value) =~ value}; self
|
377
383
|
end
|
378
|
-
|
384
|
+
|
379
385
|
def <=(value)
|
380
|
-
create_block value, lambda {|x,y| x <= y}, lambda {|x| x <= value}; self
|
386
|
+
create_block value, lambda {|x,y| x != nil && convert(x, y) <= y}, lambda {|x| x != nil && convert(x, value) <= value}; self
|
381
387
|
end
|
382
|
-
|
388
|
+
|
383
389
|
def >=(value)
|
384
|
-
create_block value, lambda {|x,y| x >= y}, lambda {|x| x >= value}; self
|
390
|
+
create_block value, lambda {|x,y| x != nil && convert(x, y) >= y}, lambda {|x| x != nil && convert(x, value) >= value}; self
|
385
391
|
end
|
386
|
-
|
392
|
+
|
393
|
+
def convert(x, value)
|
394
|
+
if x.nil?
|
395
|
+
return x
|
396
|
+
end
|
397
|
+
return x.to_f if value.kind_of? Float
|
398
|
+
return x.to_i if value.kind_of? Numeric
|
399
|
+
return x
|
400
|
+
end
|
401
|
+
|
387
402
|
def build_atoms(tags,methods,when_id)
|
388
403
|
atoms = @child_atom_builders.map { |atom_builder|
|
389
404
|
tags[atom_builder.tag] = when_id
|
@@ -398,7 +413,7 @@ module Ruleby
|
|
398
413
|
return atoms << Core::PropertyAtom.new(@tag, @name, @deftemplate, &@block)
|
399
414
|
end
|
400
415
|
end
|
401
|
-
|
416
|
+
|
402
417
|
if references_self?(tags,when_id)
|
403
418
|
bind_methods = @bindings.collect{ |bb| methods[bb.tag] }
|
404
419
|
atoms << Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@deftemplate,&@block)
|
@@ -407,7 +422,7 @@ module Ruleby
|
|
407
422
|
atoms << Core::ReferenceAtom.new(@tag,@name,bind_tags,@deftemplate,&@block)
|
408
423
|
end
|
409
424
|
end
|
410
|
-
|
425
|
+
|
411
426
|
private
|
412
427
|
def references_self?(tags,when_id)
|
413
428
|
ref_self = 0
|
@@ -416,14 +431,14 @@ module Ruleby
|
|
416
431
|
ref_self += 1
|
417
432
|
end
|
418
433
|
end
|
419
|
-
|
434
|
+
|
420
435
|
if ref_self > 0 and ref_self != @bindings.size
|
421
436
|
raise 'Binding to self and another pattern in the same condition is not yet supported.'
|
422
437
|
end
|
423
|
-
|
438
|
+
|
424
439
|
return ref_self > 0
|
425
440
|
end
|
426
|
-
|
441
|
+
|
427
442
|
def create_block(value, ref_block, basic_block)
|
428
443
|
if value && value.kind_of?(BindingBuilder)
|
429
444
|
@bindings = [value]
|
@@ -437,7 +452,7 @@ module Ruleby
|
|
437
452
|
end
|
438
453
|
end
|
439
454
|
end
|
440
|
-
|
455
|
+
|
441
456
|
class NotOperatorBuilder < AtomBuilder
|
442
457
|
def ==(value)
|
443
458
|
create_block value, lambda {|x,y| x != y}, lambda {|x| x != value}; self
|
data/lib/dsl/letigre.rb
CHANGED
metadata
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruleby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: true
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 8
|
8
|
+
- b1
|
9
|
+
version: 0.8.b1
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
|
-
- Joe Kutner
|
8
|
-
- Matt Smith
|
12
|
+
- Joe Kutner
|
13
|
+
- Matt Smith
|
9
14
|
autorequire:
|
10
15
|
bindir: bin
|
11
16
|
cert_chain: []
|
12
17
|
|
13
|
-
date:
|
18
|
+
date: 2011-03-29 00:00:00 -05:00
|
14
19
|
default_executable:
|
15
20
|
dependencies: []
|
16
21
|
|
@@ -28,17 +33,17 @@ extensions: []
|
|
28
33
|
extra_rdoc_files: []
|
29
34
|
|
30
35
|
files:
|
31
|
-
- lib/
|
32
|
-
- lib/
|
33
|
-
- lib/
|
34
|
-
- lib/core/
|
35
|
-
- lib/core/
|
36
|
-
- lib/
|
37
|
-
- lib/
|
38
|
-
- lib/
|
39
|
-
- lib/
|
40
|
-
- lib/
|
41
|
-
- lib/
|
36
|
+
- lib/rule_helper.rb
|
37
|
+
- lib/rulebook.rb
|
38
|
+
- lib/ruleby.rb
|
39
|
+
- lib/core/atoms.rb
|
40
|
+
- lib/core/engine.rb
|
41
|
+
- lib/core/nodes.rb
|
42
|
+
- lib/core/patterns.rb
|
43
|
+
- lib/core/utils.rb
|
44
|
+
- lib/dsl/ferrari.rb
|
45
|
+
- lib/dsl/letigre.rb
|
46
|
+
- lib/dsl/steel.rb
|
42
47
|
has_rdoc: true
|
43
48
|
homepage: http://ruleby.org
|
44
49
|
licenses: []
|
@@ -47,25 +52,31 @@ post_install_message:
|
|
47
52
|
rdoc_options: []
|
48
53
|
|
49
54
|
require_paths:
|
50
|
-
- lib
|
55
|
+
- lib
|
51
56
|
required_ruby_version: !ruby/object:Gem::Requirement
|
52
57
|
requirements:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
segments:
|
61
|
+
- 1
|
62
|
+
- 8
|
63
|
+
- 2
|
64
|
+
version: 1.8.2
|
57
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
66
|
requirements:
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
67
|
+
- - ">"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 1
|
71
|
+
- 3
|
72
|
+
- 1
|
73
|
+
version: 1.3.1
|
63
74
|
requirements: []
|
64
75
|
|
65
76
|
rubyforge_project: ruleby
|
66
|
-
rubygems_version: 1.3.
|
77
|
+
rubygems_version: 1.3.6
|
67
78
|
signing_key:
|
68
79
|
specification_version: 3
|
69
80
|
summary: Rete based Ruby Rule Engine
|
70
81
|
test_files:
|
71
|
-
- tests/test.rb
|
82
|
+
- tests/test.rb
|