mini_kraken 0.1.07 → 0.1.12

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +11 -3
  4. data/lib/mini_kraken/core/association_walker.rb +1 -1
  5. data/lib/mini_kraken/core/base_arg.rb +10 -0
  6. data/lib/mini_kraken/core/conde.rb +143 -0
  7. data/lib/mini_kraken/core/conj2.rb +59 -53
  8. data/lib/mini_kraken/core/def_relation.rb +49 -0
  9. data/lib/mini_kraken/core/disj2.rb +53 -48
  10. data/lib/mini_kraken/core/duck_fiber.rb +1 -1
  11. data/lib/mini_kraken/core/equals.rb +134 -126
  12. data/lib/mini_kraken/core/fail.rb +18 -14
  13. data/lib/mini_kraken/core/formal_arg.rb +22 -0
  14. data/lib/mini_kraken/core/formal_ref.rb +24 -0
  15. data/lib/mini_kraken/core/goal.rb +9 -3
  16. data/lib/mini_kraken/core/goal_arg.rb +5 -3
  17. data/lib/mini_kraken/core/goal_template.rb +60 -0
  18. data/lib/mini_kraken/core/k_boolean.rb +31 -0
  19. data/lib/mini_kraken/core/outcome.rb +14 -0
  20. data/lib/mini_kraken/core/relation.rb +7 -0
  21. data/lib/mini_kraken/core/succeed.rb +17 -13
  22. data/lib/mini_kraken/core/vocabulary.rb +22 -4
  23. data/lib/mini_kraken/glue/fresh_env.rb +45 -3
  24. data/lib/mini_kraken/glue/run_star_expression.rb +43 -26
  25. data/lib/mini_kraken/version.rb +1 -1
  26. data/spec/core/conde_spec.rb +147 -0
  27. data/spec/core/conj2_spec.rb +8 -9
  28. data/spec/core/def_relation_spec.rb +96 -0
  29. data/spec/core/disj2_spec.rb +0 -45
  30. data/spec/core/duck_fiber_spec.rb +12 -1
  31. data/spec/core/equals_spec.rb +3 -3
  32. data/spec/core/goal_template_spec.rb +74 -0
  33. data/spec/core/k_boolean_spec.rb +107 -0
  34. data/spec/core/outcome_spec.rb +48 -0
  35. data/spec/core/vocabulary_spec.rb +6 -0
  36. data/spec/glue/fresh_env_spec.rb +27 -1
  37. data/spec/glue/run_star_expression_spec.rb +535 -18
  38. data/spec/mini_kraken_spec.rb +2 -0
  39. data/spec/spec_helper.rb +0 -1
  40. data/spec/support/factory_methods.rb +15 -0
  41. metadata +19 -2
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'atomic_term'
4
+
5
+ module MiniKraken
6
+ module Core
7
+ # A specialized atomic term that represents an boolean (true/false) value.
8
+ # in MiniKraken
9
+ class KBoolean < AtomicTerm
10
+ # @param aValue [Boolean, Symbol] Ruby representation of boolean value
11
+ def initialize(aValue)
12
+ super(validated_value(aValue))
13
+ end
14
+
15
+ private
16
+
17
+ def validated_value(aValue)
18
+ case aValue
19
+ when true, false
20
+ aValue
21
+ when :"#t", '#t'
22
+ true
23
+ when :"#f", '#f'
24
+ false
25
+ else
26
+ raise StandardError, "Invalid boolean literal '#{aValue}'"
27
+ end
28
+ end
29
+ end # class
30
+ end # module
31
+ end # module
@@ -16,6 +16,14 @@ unless MiniKraken::Core.constants(false).include? :Outcome
16
16
  @resultant = aResult
17
17
  end
18
18
 
19
+ def self.failure(aParent = nil)
20
+ new(:"#u", aParent)
21
+ end
22
+
23
+ def self.success(aParent = nil)
24
+ new(:"#s", aParent)
25
+ end
26
+
19
27
  def successful?
20
28
  resultant == :"#s"
21
29
  end
@@ -30,6 +38,12 @@ unless MiniKraken::Core.constants(false).include? :Outcome
30
38
 
31
39
  are_equal
32
40
  end
41
+
42
+ protected
43
+
44
+ def introspect
45
+ ", @resultant=#{resultant}"
46
+ end
33
47
  end # class
34
48
 
35
49
  Failure = Outcome.new(:"#u")
@@ -16,6 +16,13 @@ module MiniKraken
16
16
  @alt_name = alternateName
17
17
  end
18
18
 
19
+ # A relation is polyadic when it accepts an arbitrary number of arguments.
20
+ # Most built-in relation takes a fixed number of arguments (= arity).
21
+ # @return [Boolean]
22
+ def polyadic?
23
+ false
24
+ end
25
+
19
26
  # Number of arguments for the relation.
20
27
  # @return [Integer]
21
28
  def arity
@@ -4,19 +4,23 @@ require 'singleton'
4
4
  require_relative 'duck_fiber'
5
5
  require_relative 'nullary_relation'
6
6
 
7
- module MiniKraken
8
- module Core
9
- # A nullary relation that unconditionally always fails.
10
- class Succeed < NullaryRelation
11
- include Singleton
7
+ unless MiniKraken::Core.constants(false).include? :Succeed
8
+ module MiniKraken
9
+ module Core
10
+ # A nullary relation that unconditionally always fails.
11
+ class Succeed < NullaryRelation
12
+ include Singleton
12
13
 
13
- def initialize
14
- super('succeed', '#s')
15
- end
14
+ def initialize
15
+ super('succeed', '#s')
16
+ end
16
17
 
17
- def solver_for(_actuals, _env)
18
- DuckFiber.new(:success)
19
- end
20
- end # class
18
+ def solver_for(_actuals, _env)
19
+ DuckFiber.new(:success)
20
+ end
21
+ end # class
22
+
23
+ Succeed.instance.freeze
24
+ end # module
21
25
  end # module
22
- end # module
26
+ end # unless
@@ -67,20 +67,22 @@ module MiniKraken
67
67
 
68
68
  raise StandardError unless orphan
69
69
 
70
+ rank = nil
70
71
  if orphan.rankings.include?(aName)
71
- orphan.rankings[aName]
72
+ rank = orphan.rankings[aName]
72
73
  else
73
74
  other = alternate_names.find do |a_name|
74
- orphan.rankings.include?(a_name)
75
+ rank = orphan.rankings.include?(a_name)
75
76
  end
76
77
  if other
77
- get_rank(other)
78
+ rank = get_rank(other)
78
79
  else
79
80
  rank = orphan.rankings.keys.size
80
81
  orphan.rankings[aName] = rank
81
- rank
82
82
  end
83
83
  end
84
+
85
+ rank
84
86
  end
85
87
 
86
88
  # Record an association between a variable with given user-defined name
@@ -392,6 +394,18 @@ module MiniKraken
392
394
  name2var(aVarName) ? true : false
393
395
  end
394
396
 
397
+ def inspect
398
+ result = +"#<#{self.class.name}:#{object_id.to_s(16)} @parent="
399
+ if parent
400
+ result << "#<#{parent.class.name}:#{parent.object_id.to_s(16)}>"
401
+ else
402
+ result << nil
403
+ end
404
+ result << introspect
405
+ result << '>'
406
+ result
407
+ end
408
+
395
409
  protected
396
410
 
397
411
  def validated_parent(aParent)
@@ -418,6 +432,10 @@ module MiniKraken
418
432
  assc
419
433
  end
420
434
  end
435
+
436
+ def introspect
437
+ ''
438
+ end
421
439
  end # class
422
440
  end # module
423
441
  end # module
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../core/environment'
4
+ require_relative '../core/conj2'
4
5
  require_relative '../core/variable'
5
6
 
6
7
  module MiniKraken
7
8
  module Glue
9
+ # A combination of an Environment (= a scope for one or more variables)
10
+ # and a goal. It quacks like a Goal object: when receiving the attain message,
11
+ # it attempt to achieve its given goal.
8
12
  # (fresh (x) (== 'pea q))
9
13
  # Introduces the new variable 'x'
10
14
  # Takes a list of names and a goal-like object
@@ -13,11 +17,11 @@ module MiniKraken
13
17
  # @return [Goal]
14
18
  attr_reader :goal
15
19
 
16
- # @param theNames [Array<String>]
17
- # @param aGoal [Goal]
20
+ # @param theNames [Array<String>] The variable names
21
+ # @param aGoal [Goal, Array<Goal>] The goal to achieve or the conjunction of them.
18
22
  def initialize(theNames, aGoal)
19
23
  super()
20
- @goal = aGoal
24
+ @goal = valid_goal(aGoal)
21
25
  theNames.each { |nm| add_var(Core::Variable.new(nm)) }
22
26
  end
23
27
 
@@ -28,6 +32,44 @@ module MiniKraken
28
32
  self.parent = aParent
29
33
  goal.attain(self)
30
34
  end
35
+
36
+ protected
37
+
38
+ def introspect
39
+ +", @vars=[#{vars.keys.join(', ')}]"
40
+ end
41
+
42
+ private
43
+
44
+ def valid_goal(aGoal)
45
+ result = nil
46
+
47
+ case aGoal
48
+ when Core::Goal
49
+ result = aGoal
50
+ when FreshEnv
51
+ result = aGoal
52
+ when Array # an Array of Goal?..
53
+ goal_array = aGoal
54
+ loop do
55
+ conjunctions = []
56
+ goal_array.each_slice(2) do |uno_duo|
57
+ if uno_duo.size == 2
58
+ conjunctions << Core::Goal.new(Core::Conj2.instance, uno_duo)
59
+ else
60
+ conjunctions << uno_duo[0]
61
+ end
62
+ end
63
+ if conjunctions.size == 1
64
+ result = conjunctions[0]
65
+ break
66
+ end
67
+ goal_array = conjunctions
68
+ end
69
+ end
70
+
71
+ result
72
+ end
31
73
  end # class
32
74
  end # module
33
75
  end # module
@@ -7,21 +7,18 @@ require_relative 'fresh_env'
7
7
  module MiniKraken
8
8
  module Glue
9
9
  class RunStarExpression
10
+ # @return [FreshEnv] The environment in which run* variables will reside.
10
11
  attr_reader :env
11
12
 
12
- # @param var_name [String]
13
- # @param goal [Core::Goal]
14
- def initialize(var_name, goal)
15
- @env = FreshEnv.new([var_name], goal)
16
- end
17
-
18
- def var
19
- env.vars.values.first
13
+ # @param var_names [String, Array<String>] One variable name or an array of names
14
+ # @param goal [Core::Goal, Array<Core::Goal>] A single goal or an array of goals to conjunct
15
+ def initialize(var_names, goal)
16
+ vnames = var_names.kind_of?(String) ? [var_names] : var_names
17
+ @env = FreshEnv.new(vnames, goal)
20
18
  end
21
19
 
22
20
  def run
23
- result = nil
24
- next_result = nil
21
+ result = []
25
22
  solver = env.goal.attain(env)
26
23
  # require 'debug'
27
24
  loop do
@@ -30,24 +27,44 @@ module MiniKraken
30
27
  outcome = solver.resume
31
28
  break if outcome.nil?
32
29
 
33
- if result # ... more than one result...
34
- if outcome.successful?
35
- next_result.append(Core::ConsCell.new(var.quote(outcome)))
36
- else
37
- next_result.append(Core::NullList)
38
- end
39
- next_result = next_result.cdr
40
- elsif outcome.successful?
41
- env.propagate(outcome)
42
- result = Core::ConsCell.new(var.quote(outcome))
43
- next_result = result
44
- else
45
- result = Core::NullList
46
- next_result = result
47
- end
30
+ env.propagate(outcome) if result.empty? && outcome.successful?
31
+ result << build_solution(outcome)
32
+ end
33
+
34
+ format_solutions(result)
35
+ end
36
+
37
+ private
38
+
39
+ # @return [Array] A vector of assignment for each variable
40
+ def build_solution(outcome)
41
+ sol = env.vars.values.map do |var|
42
+ outcome.successful? ? var.quote(outcome) : nil
43
+ end
44
+
45
+ sol
46
+ end
47
+
48
+ # Transform the solutions into sequence of conscells.
49
+ # @param solutions [Array<Array>] An array of solution.
50
+ # A solution is in itself an array of bindings (one per variable)
51
+ def format_solutions(solutions)
52
+ solutions_as_list = solutions.map { |sol| arr2list(sol, true) }
53
+ arr2list(solutions_as_list, false)
54
+ end
55
+
56
+ # Utility method. Transform an array into a ConsCell-based list.
57
+ # @param anArray [Array]
58
+ # @param simplify [Boolean]
59
+ def arr2list(anArray, simplify)
60
+ return anArray[0] if anArray.size == 1 && simplify
61
+
62
+ new_tail = nil
63
+ anArray.reverse_each do |elem|
64
+ new_tail = Core::ConsCell.new(elem, new_tail)
48
65
  end
49
66
 
50
- result
67
+ new_tail
51
68
  end
52
69
  end # class
53
70
  end # module
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniKraken
4
- VERSION = '0.1.07'
4
+ VERSION = '0.1.12'
5
5
  end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper' # Use the RSpec framework
4
+ require_relative '../../lib/mini_kraken/core/k_symbol'
5
+ require_relative '../../lib/mini_kraken/core/fail'
6
+ require_relative '../../lib/mini_kraken/core/succeed'
7
+ require_relative '../../lib/mini_kraken/core/equals'
8
+ require_relative '../../lib/mini_kraken/core/environment'
9
+ require_relative '../../lib/mini_kraken/core/variable'
10
+ require_relative '../../lib/mini_kraken/core/variable_ref'
11
+
12
+ # Load the class under test
13
+ require_relative '../../lib/mini_kraken/core/conde'
14
+
15
+ module MiniKraken
16
+ module Core
17
+ describe Conde do
18
+ subject { Conde.instance }
19
+
20
+ context 'Initialization:' do
21
+ it 'should be initialized without argument' do
22
+ expect { Conde.instance }.not_to raise_error
23
+ end
24
+
25
+ it 'should know its name' do
26
+ expect(subject.name).to eq('conde')
27
+ end
28
+ end # context
29
+
30
+ context 'Provided services:' do
31
+ let(:bean) { KSymbol.new(:bean) }
32
+ let(:corn) { KSymbol.new(:corn) }
33
+ let(:meal) { KSymbol.new(:meal) }
34
+ let(:oil) { KSymbol.new(:oil) }
35
+ let(:olive) { KSymbol.new(:olive) }
36
+ let(:pea) { KSymbol.new(:pea) }
37
+ let(:red) { KSymbol.new(:red) }
38
+ let(:split) { KSymbol.new(:split) }
39
+ let(:fails) { Goal.new(Fail.instance, []) }
40
+ let(:succeeds) { Goal.new(Succeed.instance, []) }
41
+ let(:var_q) { Variable.new('q') }
42
+ let(:var_x) { Variable.new('x') }
43
+ let(:var_y) { Variable.new('y') }
44
+ let(:ref_q) { VariableRef.new('q') }
45
+ let(:ref_x) { VariableRef.new('x') }
46
+ let(:ref_y) { VariableRef.new('y') }
47
+ let(:env) do
48
+ e = Environment.new
49
+ e.add_var(var_q)
50
+ e.add_var(var_x)
51
+ e.add_var(var_y)
52
+ e
53
+ end
54
+
55
+ it 'should complain when one of its argument is not a goal' do
56
+ err = StandardError
57
+ expect { subject.solver_for([succeeds, pea], env) }.to raise_error(err)
58
+ expect { subject.solver_for([pea, succeeds], env) }.to raise_error(err)
59
+ end
60
+
61
+ it 'should fails if when all goals fail' do
62
+ solver = subject.solver_for([fails, fails, fails], env)
63
+ expect(solver.resume).not_to be_successful
64
+ expect(solver.resume).to be_nil
65
+ end
66
+
67
+ it 'yield success if first argument succeeds' do
68
+ subgoal = Goal.new(Equals.instance, [olive, ref_q])
69
+ solver = subject.solver_for([subgoal, fails, fails], env)
70
+ outcome = solver.resume
71
+ expect(outcome).to be_successful
72
+ expect(outcome.associations['q'].first.value).to eq(olive)
73
+ expect(solver.resume).to be_nil
74
+ end
75
+
76
+ it 'yield success if second argument succeeds' do
77
+ subgoal = Goal.new(Equals.instance, [oil, ref_q])
78
+ solver = subject.solver_for([fails, subgoal, fails], env)
79
+ outcome = solver.resume
80
+ expect(outcome).to be_successful
81
+ expect(outcome.associations['q'].first.value).to eq(oil)
82
+ expect(solver.resume).to be_nil
83
+ end
84
+
85
+ it 'yield success if third argument succeeds' do
86
+ subgoal = Goal.new(Equals.instance, [oil, ref_q])
87
+ solver = subject.solver_for([fails, fails, subgoal], env)
88
+ outcome = solver.resume
89
+ expect(outcome).to be_successful
90
+ expect(outcome.associations['q'].first.value).to eq(oil)
91
+ expect(solver.resume).to be_nil
92
+ end
93
+
94
+ it 'yields three solutions if three goals succeed' do
95
+ # Covers frame 1:58
96
+ subgoal1 = Goal.new(Equals.instance, [olive, ref_q])
97
+ subgoal2 = Goal.new(Equals.instance, [oil, ref_q])
98
+ subgoal3 = Goal.new(Equals.instance, [pea, ref_q])
99
+ solver = subject.solver_for([subgoal1, subgoal2, subgoal3, fails], env)
100
+
101
+ # First solution
102
+ outcome1 = solver.resume
103
+ expect(outcome1).to be_successful
104
+ expect(outcome1.associations['q'].first.value).to eq(olive)
105
+
106
+ # Second solution
107
+ outcome2 = solver.resume
108
+ expect(outcome2).to be_successful
109
+ expect(outcome2.associations['q'].first.value).to eq(oil)
110
+
111
+ # Third solution
112
+ outcome3 = solver.resume
113
+ expect(outcome3).to be_successful
114
+ expect(outcome3.associations['q'].first.value).to eq(pea)
115
+
116
+ expect(solver.resume).to be_nil
117
+ end
118
+
119
+ it 'also use conjunctions for nested goals' do
120
+ # Covers frame 1:88
121
+ subgoal1 = Goal.new(Equals.instance, [split, ref_x])
122
+ subgoal2 = Goal.new(Equals.instance, [pea, ref_y])
123
+ combo1 = [subgoal1, subgoal2]
124
+
125
+ subgoal3 = Goal.new(Equals.instance, [red, ref_x])
126
+ subgoal4 = Goal.new(Equals.instance, [bean, ref_y])
127
+ combo2 = [subgoal3, subgoal4]
128
+ solver = subject.solver_for([combo1, combo2], env)
129
+
130
+ # First solution
131
+ outcome1 = solver.resume
132
+ expect(outcome1).to be_successful
133
+ expect(outcome1.associations['x'].first.value).to eq(split)
134
+ expect(outcome1.associations['y'].first.value).to eq(pea)
135
+
136
+ # Second solution
137
+ outcome2 = solver.resume
138
+ expect(outcome2).to be_successful
139
+ expect(outcome2.associations['x'].first.value).to eq(red)
140
+ expect(outcome2.associations['y'].first.value).to eq(bean)
141
+
142
+ expect(solver.resume).to be_nil
143
+ end
144
+ end # context
145
+ end # describe
146
+ end # module
147
+ end # module