wongi-engine 0.0.7 → 0.0.8
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.
- checksums.yaml +4 -4
- data/README.md +38 -9
- data/lib/wongi-engine/beta/beta_node.rb +2 -2
- data/lib/wongi-engine/beta/join_node.rb +1 -1
- data/lib/wongi-engine/beta/neg_node.rb +3 -3
- data/lib/wongi-engine/dsl.rb +8 -8
- data/lib/wongi-engine/network.rb +1 -1
- data/lib/wongi-engine/template.rb +8 -5
- data/lib/wongi-engine/token.rb +5 -1
- data/lib/wongi-engine/version.rb +1 -1
- data/spec/high_level_spec.rb +2 -2
- data/spec/rule_specs/negative_rule_spec.rb +17 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42749e9f8fc743ae3e5a96e43d44185446c622c7
|
4
|
+
data.tar.gz: 55c35cd79c0ea900a27d173b76c4f949750f0f50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d1a6fa593a0d293bcb563c8ba0f1388541a12f44f0a7ec8de279a98b8f8cda8faa49a26d514f67f3eaff366e1a889561da0672ec0129a0c466e75ab1e2f1b0b
|
7
|
+
data.tar.gz: edb677ce97d0a79e986f1646350d6f358569e3ce3d070e92c437ca540b4b23758a990056df6e08c99676e3de6f0e9230808909b67f09f2896e7c5798e99d982e
|
data/README.md
CHANGED
@@ -213,9 +213,34 @@ Passes if the block evaluates to `true`. Having no arguments passes the entire t
|
|
213
213
|
|
214
214
|
Not a *matcher*, strictly speaking, because it always passes. What it does instead is introduce a new variable bound to the block's return value.
|
215
215
|
|
216
|
+
### Feedback loop prevention
|
217
|
+
|
218
|
+
Consider the following rule:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
engine.rule "default value" do
|
222
|
+
forall {
|
223
|
+
neg :car, :colour, :_
|
224
|
+
}
|
225
|
+
make {
|
226
|
+
gen :car, :colour, "black"
|
227
|
+
}
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
The intent here is to provide a default value for an attribute; however, how will it actually execute?
|
232
|
+
|
233
|
+
1. The fact is missing, activate the rule, generate the fact.
|
234
|
+
2. The fact is present, invalidate the rule, retract the generated fact.
|
235
|
+
3. The fact is missing...
|
236
|
+
|
237
|
+
...and so on until you get a stack overflow. In situations like this the engine will try to do the pragmatic thing and detect the loop, stopping after step 1. If you want to keep the "pure" behaviour, give the `unsafe: true` option to the `neg` rule and try to do the same thing with helper facts.
|
238
|
+
|
239
|
+
**Note**: this is a specific use case; it is still perfectly possible to construct more elaborate infinite cascades involving several rules that will not be caught.
|
240
|
+
|
216
241
|
### Timeline
|
217
242
|
|
218
|
-
Wongi::Engine has a limited concept of timed facts: time is discrete and only extends into the past. Matchers that accept a triple specification (`has`, `neg` and `maybe`) can also accept a
|
243
|
+
Wongi::Engine has a limited concept of timed facts: time is discrete and only extends into the past. Matchers that accept a triple specification (`has`, `neg` and `maybe`) can also accept a `time` option, an integer <= 0, which will make them look at a past state of the system. "0" means the current state and is the default value, "-1" means the one just before the current, and so on.
|
219
244
|
|
220
245
|
To create past states, say:
|
221
246
|
|
@@ -234,8 +259,8 @@ The following matchers are nothing but syntactic sugar for a combination of prim
|
|
234
259
|
Short for:
|
235
260
|
|
236
261
|
```ruby
|
237
|
-
neg subject, predicate, object, -1
|
238
|
-
has subject, predicate, object, 0
|
262
|
+
neg subject, predicate, object, time: -1
|
263
|
+
has subject, predicate, object, time: 0
|
239
264
|
```
|
240
265
|
|
241
266
|
That is, it passes if the fact was missing in the previous state but exists in the current one. Alias: `added`.
|
@@ -245,8 +270,8 @@ That is, it passes if the fact was missing in the previous state but exists in t
|
|
245
270
|
Short for:
|
246
271
|
|
247
272
|
```ruby
|
248
|
-
has subject, predicate, object, -1
|
249
|
-
neg subject, predicate, object, 0
|
273
|
+
has subject, predicate, object, time: -1
|
274
|
+
neg subject, predicate, object, time: 0
|
250
275
|
```
|
251
276
|
|
252
277
|
The reverse of `asserted`. Alias: `removed`.
|
@@ -256,8 +281,8 @@ The reverse of `asserted`. Alias: `removed`.
|
|
256
281
|
Short for:
|
257
282
|
|
258
283
|
```ruby
|
259
|
-
has subject, predicate, object, -1
|
260
|
-
has subject, predicate, object, 0
|
284
|
+
has subject, predicate, object, time: -1
|
285
|
+
has subject, predicate, object, time: 0
|
261
286
|
```
|
262
287
|
|
263
288
|
Alias: `still_has`.
|
@@ -267,8 +292,8 @@ Alias: `still_has`.
|
|
267
292
|
Short for:
|
268
293
|
|
269
294
|
```ruby
|
270
|
-
neg subject, predicate, object, -1
|
271
|
-
neg subject, predicate, object, 0
|
295
|
+
neg subject, predicate, object, time: -1
|
296
|
+
neg subject, predicate, object, time: 0
|
272
297
|
```
|
273
298
|
|
274
299
|
Alias: `still_missing`.
|
@@ -421,6 +446,10 @@ The Rete implementation in this library largely follows the outline presented in
|
|
421
446
|
|
422
447
|
## Changelog
|
423
448
|
|
449
|
+
### 0.0.8
|
450
|
+
|
451
|
+
* preventing the feedback loop introduced in 0.0.7; experimental
|
452
|
+
|
424
453
|
### 0.0.7
|
425
454
|
|
426
455
|
* added a guard against introducing variables in neg clauses
|
@@ -77,8 +77,8 @@ module Wongi::Engine
|
|
77
77
|
node
|
78
78
|
end
|
79
79
|
|
80
|
-
def neg_node alpha, tests, alpha_deaf
|
81
|
-
node = NegNode.new self, tests, alpha
|
80
|
+
def neg_node alpha, tests, alpha_deaf, unsafe
|
81
|
+
node = NegNode.new self, tests, alpha, unsafe
|
82
82
|
alpha.betas << node unless alpha_deaf
|
83
83
|
node.refresh
|
84
84
|
node
|
@@ -91,7 +91,7 @@ module Wongi
|
|
91
91
|
|
92
92
|
def self.compile condition, earlier, parameters
|
93
93
|
tests = []
|
94
|
-
assignment = Template.new
|
94
|
+
assignment = Template.new :_, :_, :_
|
95
95
|
[:subject, :predicate, :object].each do |field|
|
96
96
|
member = condition.send field
|
97
97
|
if Template.variable?( member )
|
@@ -7,15 +7,15 @@ module Wongi
|
|
7
7
|
|
8
8
|
attr_reader :tokens, :alpha, :tests
|
9
9
|
|
10
|
-
def initialize parent, tests, alpha
|
10
|
+
def initialize parent, tests, alpha, unsafe
|
11
11
|
super(parent)
|
12
|
-
@tests, @alpha = tests, alpha
|
12
|
+
@tests, @alpha, @unsafe = tests, alpha, unsafe
|
13
13
|
@tokens = []
|
14
14
|
end
|
15
15
|
|
16
16
|
def alpha_activate wme
|
17
17
|
self.tokens.each do |token|
|
18
|
-
if matches?( token, wme )
|
18
|
+
if matches?( token, wme ) && ( @unsafe || ! token.generated?( wme ) )# feedback loop protection
|
19
19
|
# order matters for proper invalidation
|
20
20
|
make_join_result(token, wme)
|
21
21
|
token.delete_children #if token.neg_join_results.empty? # TODO why was this check here? it seems to break things
|
data/lib/wongi-engine/dsl.rb
CHANGED
@@ -80,26 +80,26 @@ dsl {
|
|
80
80
|
|
81
81
|
clause :asserted, :added
|
82
82
|
body { |s, p, o|
|
83
|
-
missing s, p, o, -1
|
84
|
-
has s, p, o, 0
|
83
|
+
missing s, p, o, time: -1
|
84
|
+
has s, p, o, time: 0
|
85
85
|
}
|
86
86
|
|
87
87
|
clause :retracted, :removed
|
88
88
|
body { |s, p, o|
|
89
|
-
has s, p, o, -1
|
90
|
-
missing s, p, o, 0
|
89
|
+
has s, p, o, time: -1
|
90
|
+
missing s, p, o, time: 0
|
91
91
|
}
|
92
92
|
|
93
93
|
clause :kept, :still_has
|
94
94
|
body { |s, p, o|
|
95
|
-
has s, p, o, -1
|
96
|
-
has s, p, o, 0
|
95
|
+
has s, p, o, time: -1
|
96
|
+
has s, p, o, time: 0
|
97
97
|
}
|
98
98
|
|
99
99
|
clause :kept_missing, :still_missing
|
100
100
|
body { |s, p, o|
|
101
|
-
missing s, p, o, -1
|
102
|
-
missing s, p, o, 0
|
101
|
+
missing s, p, o, time: -1
|
102
|
+
missing s, p, o, time: 0
|
103
103
|
}
|
104
104
|
|
105
105
|
|
data/lib/wongi-engine/network.rb
CHANGED
@@ -244,7 +244,7 @@ module Wongi::Engine
|
|
244
244
|
else
|
245
245
|
if @timeline[time+1].nil?
|
246
246
|
# => ensure lineage from 0 to time
|
247
|
-
compile_alpha condition.class.new(condition.subject, condition.predicate, condition.object, time + 1)
|
247
|
+
compile_alpha condition.class.new(condition.subject, condition.predicate, condition.object, time: time + 1)
|
248
248
|
@timeline.unshift Hash.new
|
249
249
|
end
|
250
250
|
@timeline[time+1][ hash ] = alpha
|
@@ -4,22 +4,25 @@ module Wongi::Engine
|
|
4
4
|
|
5
5
|
include CoreExt
|
6
6
|
|
7
|
-
attr_reader :filters
|
7
|
+
attr_reader :filters, :unsafe
|
8
8
|
attr_predicate debug: false
|
9
9
|
|
10
10
|
def self.variable? thing
|
11
11
|
Symbol === thing && thing =~ /^[A-Z]/
|
12
12
|
end
|
13
13
|
|
14
|
-
def initialize s
|
14
|
+
def initialize s, p, o, options = { }
|
15
|
+
time = options[:time] || 0
|
16
|
+
unsafe = options[:unsafe] || false
|
15
17
|
raise "Cannot work with continuous time" unless time.integer?
|
16
18
|
raise "Cannot look into the future" if time > 0
|
17
|
-
super
|
19
|
+
super( s, p, o, time )
|
18
20
|
@filters = []
|
21
|
+
@unsafe = unsafe
|
19
22
|
end
|
20
23
|
|
21
24
|
def import_into r
|
22
|
-
copy = self.class.new r.import( subject ), r.import( predicate ), r.import( object ), time
|
25
|
+
copy = self.class.new r.import( subject ), r.import( predicate ), r.import( object ), time: time, unsafe: unsafe
|
23
26
|
@filters.each { |f| copy.filters << f }
|
24
27
|
copy
|
25
28
|
end
|
@@ -95,7 +98,7 @@ module Wongi::Engine
|
|
95
98
|
tests, assignment = *JoinNode.compile( self, context.earlier, context.parameters )
|
96
99
|
raise DefinitionError.new("Negative matches may not introduce new variables: #{assignment.variables}") unless assignment.root?
|
97
100
|
alpha = context.rete.compile_alpha( self )
|
98
|
-
context.node = context.node.neg_node( alpha, tests, context.alpha_deaf )
|
101
|
+
context.node = context.node.neg_node( alpha, tests, context.alpha_deaf, unsafe )
|
99
102
|
context.node.debug = debug?
|
100
103
|
context.earlier << self
|
101
104
|
context
|
data/lib/wongi-engine/token.rb
CHANGED
data/lib/wongi-engine/version.rb
CHANGED
data/spec/high_level_spec.rb
CHANGED
@@ -221,7 +221,7 @@ describe 'the engine' do
|
|
221
221
|
|
222
222
|
production = rete.rule {
|
223
223
|
forall {
|
224
|
-
has 1, 2, 3, -1
|
224
|
+
has 1, 2, 3, time: -1
|
225
225
|
}
|
226
226
|
}
|
227
227
|
|
@@ -237,7 +237,7 @@ describe 'the engine' do
|
|
237
237
|
|
238
238
|
production = rete.rule {
|
239
239
|
forall {
|
240
|
-
has 1, 2, 3, -1
|
240
|
+
has 1, 2, 3, time: -1
|
241
241
|
}
|
242
242
|
}
|
243
243
|
|
@@ -31,12 +31,27 @@ describe "negative rule" do
|
|
31
31
|
|
32
32
|
end
|
33
33
|
|
34
|
-
it "should create infinite feedback loops" do
|
34
|
+
it "should not create infinite feedback loops by default" do
|
35
|
+
|
36
|
+
engine << rule('feedback') {
|
37
|
+
forall {
|
38
|
+
neg :a, :b, :_
|
39
|
+
}
|
40
|
+
make {
|
41
|
+
gen :a, :b, :c
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
engine.should have(1).facts
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should create infinite feedback loops with unsafe option" do
|
35
50
|
|
36
51
|
proc = lambda {
|
37
52
|
engine << rule('feedback') {
|
38
53
|
forall {
|
39
|
-
neg :a, :b, :_
|
54
|
+
neg :a, :b, :_, unsafe: true
|
40
55
|
}
|
41
56
|
make {
|
42
57
|
gen :a, :b, :c
|