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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72c7ec74580f897b110a1c0f119805d82f2e0257
4
- data.tar.gz: 0a4cea942993957a9a353ab5712cea0a44cb5bfa
3
+ metadata.gz: 42749e9f8fc743ae3e5a96e43d44185446c622c7
4
+ data.tar.gz: 55c35cd79c0ea900a27d173b76c4f949750f0f50
5
5
  SHA512:
6
- metadata.gz: dbbbc1de9d70f925ba7dfdd9d29199528cbca93ed4e11ae886412392ff23b6f614074d0169ffac8b627bb699f64850aaf82f8a792dbcafd2d719e41cb02296ee
7
- data.tar.gz: e59ac0a7bd484f94f00e65ad2e7c52d2ce5609a54c1e3a0fd8d1fc9c1b1d1d1c195e6b97199f70a59407b630bec5fffa681505c0f56ab6a29d1684f4f95abab4
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 fourth parameter, 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.
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
@@ -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
 
@@ -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 = :_, p = :_, o = :_, time = 0
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
@@ -75,8 +75,12 @@ module Wongi::Engine
75
75
  end
76
76
  end
77
77
 
78
- protected
78
+ def generated? wme
79
+ return true if generated_wmes.any? { |w| w == wme }
80
+ return children.any? { |t| t.generated? wme }
81
+ end
79
82
 
83
+ protected
80
84
 
81
85
  def retract_generated
82
86
 
@@ -1,5 +1,5 @@
1
1
  module Wongi
2
2
  module Engine
3
- VERSION = "0.0.7"
3
+ VERSION = "0.0.8"
4
4
  end
5
5
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wongi-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valeri Sokolov