wongi-engine 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72c7ec74580f897b110a1c0f119805d82f2e0257
4
+ data.tar.gz: 0a4cea942993957a9a353ab5712cea0a44cb5bfa
5
+ SHA512:
6
+ metadata.gz: dbbbc1de9d70f925ba7dfdd9d29199528cbca93ed4e11ae886412392ff23b6f614074d0169ffac8b627bb699f64850aaf82f8a792dbcafd2d719e41cb02296ee
7
+ data.tar.gz: e59ac0a7bd484f94f00e65ad2e7c52d2ce5609a54c1e3a0fd8d1fc9c1b1d1d1c195e6b97199f70a59407b630bec5fffa681505c0f56ab6a29d1684f4f95abab4
data/README.md CHANGED
@@ -37,6 +37,12 @@ engine << [ "Alice", "friend", "Bob" ]
37
37
  engine << [ "Alice", "age", 35 ]
38
38
  ```
39
39
 
40
+ To remove facts, say:
41
+
42
+ ```ruby
43
+ engine.retract [ "Alice", "age", 35 ]
44
+ ```
45
+
40
46
  What can we do with this information?
41
47
 
42
48
  ### Simple iteration
@@ -65,7 +71,7 @@ friends = engine.rule "friends" do
65
71
  end
66
72
  ```
67
73
 
68
- Here's your first taste of the engine's DSL. A rule, generally speaking, consists of a number of conditions the dataset needs to meet; those are defined in the `forall` section (also spelled `for_all`, if you prefer that). `has` (or `fact`) specifies that there needs to be a fact that matches the given pattern; in this case, one with the predicate `"friends"`.
74
+ Here's your first taste of the engine's DSL. A rule, generally speaking, consists of a number of conditions the dataset needs to meet; those are defined in the `forall` section (also spelled `for_all`, if you prefer that). `has` (or `fact`) specifies that there needs to be a fact that matches the given pattern; in this case, one with the predicate `"friend"`.
69
75
 
70
76
  When a pattern contains a symbol that starts with an uppercase letter, it introduces a variable which will be bound to an actual triple field. Their values can be retrieved from the result set:
71
77
 
@@ -143,11 +149,11 @@ engine.rule "self-printer" do
143
149
  end
144
150
  ```
145
151
 
146
- The `make` section (also spelled `do!`, if you find it more agreeable English, because `do` is a keyword in Ruby) lists everything that happens when a rule's conditions are fully matched (we say "the production node is **activated**"). Wongi::Engine provides only a small amount of built-in actions, but you can define your own ones, and the simplest one is just `action` with a block.
152
+ The `make` section (also spelled `do!`, if you find it more agreeable English, because `do` is a keyword in Ruby) lists everything that happens when a rule's conditions are fully matched (we say "the production node is **activated**"). Wongi::Engine provides only a small amount of built-in actions, but you can define your own ones, and the simplest one is just `action` with a block. The block will be executed in the engine's context.
147
153
 
148
154
  ### More facts!
149
155
 
150
- Note how our facts define relations that always go from subject to object - they form a directed graph. In a perfect world, friendships go both ways, but to specify this in out model, we need to have two facts for each couple. Instead of duplicating everything by hand, let's automate that:
156
+ Note how our facts define relations that always go from subject to object - they form a directed graph. In a perfect world, friendships go both ways, but to specify this in our model, we need to have two facts for each couple. Instead of duplicating everything by hand, let's automate that:
151
157
 
152
158
  ```ruby
153
159
  engine.rule "symmetric predicate" do
@@ -195,6 +201,10 @@ Passes if the arguments are equal. Alias: `eq`, `equal`.
195
201
 
196
202
  Passes if the arguments are not equal. Alias: `ne`.
197
203
 
204
+ #### `less x, y`, `greater x, y`
205
+
206
+ Should be obvious by now.
207
+
198
208
  #### `assert { |token| ... }`, `assert var1, var2, ... do |val1, val2, ... | ... end`
199
209
 
200
210
  Passes if the block evaluates to `true`. Having no arguments passes the entire token as an argument, listing some variables passes only their values.
@@ -411,6 +421,14 @@ The Rete implementation in this library largely follows the outline presented in
411
421
 
412
422
  ## Changelog
413
423
 
424
+ ### 0.0.7
425
+
426
+ * added a guard against introducing variables in neg clauses
427
+ * fixed execution context of simple action block (#7)
428
+ * fixed #4 once more, better
429
+ * fixed a bug with OptionalNode (#12)
430
+ * fixed behaviour of neg nodes; this will cause feedback loops when a gen action creates a fact that invalidates the action's condition
431
+
414
432
  ### 0.0.6
415
433
 
416
434
  * fixed a bug caused by retracting facts from within a rule action (#4)
data/Rakefile CHANGED
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new('spec')
7
+
8
+ # If you want to make this the default task
9
+ task :default => :spec
data/lib/wongi-engine.rb CHANGED
@@ -19,6 +19,7 @@ end
19
19
 
20
20
  require 'wongi-engine/version'
21
21
  require 'wongi-engine/core_ext'
22
+ require 'wongi-engine/error'
22
23
  require 'wongi-engine/template'
23
24
  require 'wongi-engine/wme'
24
25
  require 'wongi-engine/wme_match_data'
@@ -44,7 +44,12 @@ module Wongi::Engine
44
44
 
45
45
  def refresh_child child
46
46
  tokens.each do |token|
47
- child.beta_activate token, nil, {}
47
+ case child
48
+ when BetaMemory, NegNode
49
+ child.beta_activate token, nil, {}
50
+ else
51
+ child.beta_activate token
52
+ end
48
53
  end
49
54
  end
50
55
 
@@ -16,8 +16,9 @@ module Wongi
16
16
  def alpha_activate wme
17
17
  self.tokens.each do |token|
18
18
  if matches?( token, wme )
19
- token.delete_children if token.neg_join_results.empty?
19
+ # order matters for proper invalidation
20
20
  make_join_result(token, wme)
21
+ token.delete_children #if token.neg_join_results.empty? # TODO why was this check here? it seems to break things
21
22
  end
22
23
  end
23
24
  end
@@ -39,7 +40,7 @@ module Wongi
39
40
  end
40
41
 
41
42
  def refresh_child child
42
- tokens.each do |token|
43
+ safe_tokens.each do |token|
43
44
  if token.neg_join_results.empty?
44
45
  child.beta_activate token, nil, {}
45
46
  end
@@ -72,6 +73,15 @@ module Wongi
72
73
  token.neg_join_results << njr
73
74
  wme.neg_join_results << njr
74
75
  end
76
+
77
+ def safe_tokens
78
+ Enumerator.new do |y|
79
+ @tokens.dup.each do |token|
80
+ y << token unless token.deleted?
81
+ end
82
+ end
83
+ end
84
+
75
85
  end
76
86
  end
77
87
  end
@@ -45,6 +45,7 @@ module Wongi
45
45
  def refresh_child child
46
46
  tmp = children
47
47
  self.children = [ child ]
48
+ refresh # do the beta part
48
49
  alpha.wmes.each do |wme|
49
50
  alpha_activate wme
50
51
  end
@@ -66,6 +66,12 @@ dsl {
66
66
  clause :diff, :ne
67
67
  accept Wongi::Engine::InequalityTest
68
68
 
69
+ clause :less
70
+ accept Wongi::Engine::LessThanTest
71
+
72
+ clause :greater
73
+ accept Wongi::Engine::GreaterThanTest
74
+
69
75
  clause :assert, :dynamic
70
76
  accept Wongi::Engine::AssertingTest
71
77
 
@@ -11,7 +11,9 @@ module Wongi::Engine
11
11
  end
12
12
 
13
13
  def execute token
14
- if @action.respond_to? :call
14
+ if @action.is_a?( Proc ) || @action.respond_to?( :to_proc )
15
+ rete.instance_exec token, &@action
16
+ elsif @action.respond_to? :call
15
17
  @action.call token
16
18
  elsif @action.respond_to? :execute
17
19
  @action.execute token
@@ -31,7 +31,8 @@ module Wongi::Engine
31
31
  @template.object
32
32
  end
33
33
 
34
- wme = WME.new subject, predicate, object
34
+ # link to rete here to ensure proper linking with token
35
+ wme = WME.new subject, predicate, object, rete
35
36
 
36
37
  production.tracer.trace( action: self, wme: wme ) if production.tracer
37
38
  if existing = rete.exists?( wme )
@@ -41,9 +42,11 @@ module Wongi::Engine
41
42
  existing.generating_tokens << token
42
43
  end
43
44
  else
44
- added = rete << wme
45
- token.generated_wmes << added
46
- added.generating_tokens << token
45
+ token.generated_wmes << wme
46
+ wme.generating_tokens << token
47
+ # this MUST be done after we link the wme and the token
48
+ # in order for neg rule invalidation to work
49
+ rete << wme
47
50
  end
48
51
 
49
52
  end
@@ -0,0 +1,11 @@
1
+ module Wongi::Engine
2
+
3
+ class Error < ::Exception
4
+
5
+ end
6
+
7
+ class DefinitionError < Error
8
+
9
+ end
10
+
11
+ end
@@ -2,3 +2,5 @@ require 'wongi-engine/filter/filter_test'
2
2
  require 'wongi-engine/filter/equality_test'
3
3
  require 'wongi-engine/filter/inequality_test'
4
4
  require 'wongi-engine/filter/asserting_test'
5
+ require 'wongi-engine/filter/less_than_test'
6
+ require 'wongi-engine/filter/greater_than_test'
@@ -0,0 +1,36 @@
1
+ module Wongi::Engine
2
+
3
+ class GreaterThanTest < FilterTest
4
+
5
+ attr_reader :x, :y
6
+
7
+ def initialize x, y
8
+ @x, @y = x, y
9
+ end
10
+
11
+ def passes? token
12
+
13
+ x = if Template.variable? @x
14
+ token[@x]
15
+ else
16
+ @x
17
+ end
18
+
19
+ y = if Template.variable? @y
20
+ token[@y]
21
+ else
22
+ @y
23
+ end
24
+
25
+ return false if x == :_ || y == :_
26
+ return x > y
27
+
28
+ end
29
+
30
+ def == other
31
+ super && x == other.x && y == other.y
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,36 @@
1
+ module Wongi::Engine
2
+
3
+ class LessThanTest < FilterTest
4
+
5
+ attr_reader :x, :y
6
+
7
+ def initialize x, y
8
+ @x, @y = x, y
9
+ end
10
+
11
+ def passes? token
12
+
13
+ x = if Template.variable? @x
14
+ token[@x]
15
+ else
16
+ @x
17
+ end
18
+
19
+ y = if Template.variable? @y
20
+ token[@y]
21
+ else
22
+ @y
23
+ end
24
+
25
+ return false if x == :_ || y == :_
26
+ return x < y
27
+
28
+ end
29
+
30
+ def == other
31
+ super && x == other.x && y == other.y
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -28,6 +28,10 @@ module Wongi::Engine
28
28
  subject == :_ && predicate == :_ && object == :_
29
29
  end
30
30
 
31
+ def variables
32
+ array_form.select { |e| self.class.variable? e }
33
+ end
34
+
31
35
  def contains? var
32
36
  self.class.variable?( var ) && array_form.include?( var )
33
37
  end
@@ -88,7 +92,8 @@ module Wongi::Engine
88
92
  class NegTemplate < Template
89
93
  # :arg: context => Wongi::Rete::BetaNode::CompilationContext
90
94
  def compile context
91
- tests, _ = *JoinNode.compile( self, context.earlier, context.parameters )
95
+ tests, assignment = *JoinNode.compile( self, context.earlier, context.parameters )
96
+ raise DefinitionError.new("Negative matches may not introduce new variables: #{assignment.variables}") unless assignment.root?
92
97
  alpha = context.rete.compile_alpha( self )
93
98
  context.node = context.node.neg_node( alpha, tests, context.alpha_deaf )
94
99
  context.node.debug = debug?
@@ -15,6 +15,7 @@ module Wongi::Engine
15
15
  def initialize token, wme, assignments
16
16
  @parent, @wme, @assignments = token, wme, assignments
17
17
  @children = []
18
+ @deleted = false
18
19
  @neg_join_results = []
19
20
  @opt_join_results = []
20
21
  @ncc_results = []
@@ -39,8 +40,8 @@ module Wongi::Engine
39
40
  end
40
41
 
41
42
  def to_s
42
- str = "TOKEN [\n"
43
- all_assignments.each_pair { |key, value| str << "\t#{key} => #{value}\n" }
43
+ str = "TOKEN [ "
44
+ all_assignments.each_pair { |key, value| str << "#{key} => #{value} " }
44
45
  str << "]"
45
46
  str
46
47
  end
@@ -55,15 +56,19 @@ module Wongi::Engine
55
56
 
56
57
  def delete
57
58
  delete_children
58
- @node.tokens.delete self unless @node.kind_of?( NccPartner )
59
+ #@node.tokens.delete self unless @node.kind_of?( NccPartner )
59
60
  @wme.tokens.delete self if @wme
60
61
  @parent.children.delete self if @parent
61
62
 
62
63
  retract_generated
63
-
64
+ @deleted = true
64
65
  @node.delete_token self
65
66
  end
66
67
 
68
+ def deleted?
69
+ @deleted
70
+ end
71
+
67
72
  def delete_children
68
73
  while @children.first
69
74
  @children.first.delete
@@ -1,5 +1,5 @@
1
1
  module Wongi
2
2
  module Engine
3
- VERSION = "0.0.6"
3
+ VERSION = "0.0.7"
4
4
  end
5
5
  end
@@ -91,4 +91,50 @@ describe "issue 4" do
91
91
 
92
92
  end
93
93
 
94
+
95
+ it "should not lose track when another rule affects a set" do
96
+ engine = Wongi::Engine.create
97
+
98
+ 10.times{ |i| engine << [i, :is_number, true] }
99
+
100
+ engine.rule 'find odds' do
101
+ forall {
102
+ has :Number, :is_number, true
103
+ has :Number, :probably_odd, true
104
+ }
105
+ make {
106
+ action { |token|
107
+ number = token[:Number]
108
+ if number % 2 != 0
109
+ engine << [number, :is_odd, true]
110
+ engine.retract [number, :is_number, true]
111
+ end
112
+ }
113
+ }
114
+ end
115
+ engine.rule 'find evens' do
116
+ forall {
117
+ has :Number, :is_number, true
118
+ neg :Number, :is_odd, true
119
+ }
120
+ make {
121
+ action { |token|
122
+ number = token[:Number]
123
+ if number % 2 == 0
124
+ engine << [number, :is_even, true]
125
+ else
126
+ engine << [number, :probably_odd, true]
127
+ end
128
+ }
129
+ }
130
+ end
131
+
132
+ numbers = engine.select :_, :is_number, true
133
+ evens = engine.select :_, :is_even, true
134
+ odds = engine.select :_, :is_odd, true
135
+
136
+ # numbers.should be_empty
137
+ evens.should have(5).items
138
+ odds.should have(5).items
139
+ end
94
140
  end
@@ -66,7 +66,7 @@ describe 'the engine' do
66
66
 
67
67
  it 'should check equality' do
68
68
 
69
- rete << rule('equality') {
69
+ node = rete << rule('equality') {
70
70
  forall {
71
71
  fact :A, "same", :B
72
72
  same :A, :B
@@ -77,6 +77,42 @@ describe 'the engine' do
77
77
  }
78
78
 
79
79
  rete << [ 42, "same", 42 ]
80
+ node.should have(1).tokens
81
+
82
+ end
83
+
84
+ it 'should compare things' do
85
+
86
+ rete << rule('less') {
87
+ forall {
88
+ has :A, :age, :N1
89
+ has :B, :age, :N2
90
+ less :N1, :N2
91
+ }
92
+ make {
93
+ gen :A, :younger, :B
94
+ }
95
+ }
96
+
97
+ rete << rule('less') {
98
+ forall {
99
+ has :A, :age, :N1
100
+ has :B, :age, :N2
101
+ greater :N1, :N2
102
+ }
103
+ make {
104
+ gen :A, :older, :B
105
+ }
106
+ }
107
+
108
+ rete << ["Alice", :age, 42]
109
+ rete << ["Bob", :age, 43]
110
+
111
+ items = rete.select "Alice", :younger, "Bob"
112
+ items.should have(1).items
113
+
114
+ items = rete.select "Bob", :older, "Alice"
115
+ items.should have(1).items
80
116
 
81
117
  end
82
118
 
@@ -51,4 +51,24 @@ describe "MAYBE rule" do
51
51
 
52
52
  end
53
53
 
54
+ it "should pass with pre-added missing facts" do
55
+
56
+ engine << [1, 2, 3]
57
+
58
+ engine << rule('test') do
59
+ forall {
60
+ has 1, 2, :X
61
+ maybe :X, 4, :Y
62
+ }
63
+ end
64
+
65
+ prod = engine.productions['test']
66
+
67
+ prod.should have(1).tokens
68
+
69
+ prod.tokens.first[:X].should == 3
70
+ prod.tokens.first[:Y].should be_nil
71
+
72
+ end
73
+
54
74
  end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe "negative rule" do
4
+
5
+ before :each do
6
+ @engine = Wongi::Engine.create
7
+ end
8
+
9
+ def engine
10
+ @engine
11
+ end
12
+
13
+ it "should not introduce variables" do
14
+
15
+ proc = lambda {
16
+
17
+ engine << rule('one-option') {
18
+ forall {
19
+ neg :Foo, :bar, :_
20
+ }
21
+ make {
22
+ action { |tokens|
23
+ raise "This should never get executed #{tokens}"
24
+ }
25
+ }
26
+ }
27
+
28
+ }
29
+
30
+ proc.should raise_error( Wongi::Engine::DefinitionError )
31
+
32
+ end
33
+
34
+ it "should create infinite feedback loops" do
35
+
36
+ proc = lambda {
37
+ engine << rule('feedback') {
38
+ forall {
39
+ neg :a, :b, :_
40
+ }
41
+ make {
42
+ gen :a, :b, :c
43
+ }
44
+ }
45
+ }
46
+
47
+ proc.should raise_error( SystemStackError )
48
+
49
+ end
50
+
51
+ end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wongi-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
5
- prerelease:
4
+ version: 0.0.7
6
5
  platform: ruby
7
6
  authors:
8
7
  - Valeri Sokolov
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-24 00:00:00.000000000 Z
11
+ date: 2013-10-10 00:00:00.000000000 Z
13
12
  dependencies: []
14
13
  description: A rule engine.
15
14
  email:
@@ -60,11 +59,14 @@ files:
60
59
  - lib/wongi-engine/dsl/ncc_production_rule.rb
61
60
  - lib/wongi-engine/dsl/production_rule.rb
62
61
  - lib/wongi-engine/dsl/query.rb
62
+ - lib/wongi-engine/error.rb
63
63
  - lib/wongi-engine/filter.rb
64
64
  - lib/wongi-engine/filter/asserting_test.rb
65
65
  - lib/wongi-engine/filter/equality_test.rb
66
66
  - lib/wongi-engine/filter/filter_test.rb
67
+ - lib/wongi-engine/filter/greater_than_test.rb
67
68
  - lib/wongi-engine/filter/inequality_test.rb
69
+ - lib/wongi-engine/filter/less_than_test.rb
68
70
  - lib/wongi-engine/graph.rb
69
71
  - lib/wongi-engine/model_context.rb
70
72
  - lib/wongi-engine/network.rb
@@ -85,6 +87,7 @@ files:
85
87
  - spec/rule_specs/assign_spec.rb
86
88
  - spec/rule_specs/maybe_rule_spec.rb
87
89
  - spec/rule_specs/ncc_spec.rb
90
+ - spec/rule_specs/negative_rule_spec.rb
88
91
  - spec/ruleset_spec.rb
89
92
  - spec/simple_action_spec.rb
90
93
  - spec/spec_helper.rb
@@ -92,27 +95,26 @@ files:
92
95
  - wongi-engine.gemspec
93
96
  homepage: https://github.com/ulfurinn/wongi-engine
94
97
  licenses: []
98
+ metadata: {}
95
99
  post_install_message:
96
100
  rdoc_options: []
97
101
  require_paths:
98
102
  - lib
99
103
  required_ruby_version: !ruby/object:Gem::Requirement
100
- none: false
101
104
  requirements:
102
- - - ! '>='
105
+ - - '>='
103
106
  - !ruby/object:Gem::Version
104
107
  version: '0'
105
108
  required_rubygems_version: !ruby/object:Gem::Requirement
106
- none: false
107
109
  requirements:
108
- - - ! '>='
110
+ - - '>='
109
111
  - !ruby/object:Gem::Version
110
112
  version: '0'
111
113
  requirements: []
112
114
  rubyforge_project:
113
- rubygems_version: 1.8.24
115
+ rubygems_version: 2.0.3
114
116
  signing_key:
115
- specification_version: 3
117
+ specification_version: 4
116
118
  summary: A rule engine.
117
119
  test_files:
118
120
  - spec/bug_specs/issue_4_spec.rb
@@ -124,6 +126,7 @@ test_files:
124
126
  - spec/rule_specs/assign_spec.rb
125
127
  - spec/rule_specs/maybe_rule_spec.rb
126
128
  - spec/rule_specs/ncc_spec.rb
129
+ - spec/rule_specs/negative_rule_spec.rb
127
130
  - spec/ruleset_spec.rb
128
131
  - spec/simple_action_spec.rb
129
132
  - spec/spec_helper.rb