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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +11 -3
- data/lib/mini_kraken/core/association_walker.rb +1 -1
- data/lib/mini_kraken/core/base_arg.rb +10 -0
- data/lib/mini_kraken/core/conde.rb +143 -0
- data/lib/mini_kraken/core/conj2.rb +59 -53
- data/lib/mini_kraken/core/def_relation.rb +49 -0
- data/lib/mini_kraken/core/disj2.rb +53 -48
- data/lib/mini_kraken/core/duck_fiber.rb +1 -1
- data/lib/mini_kraken/core/equals.rb +134 -126
- data/lib/mini_kraken/core/fail.rb +18 -14
- data/lib/mini_kraken/core/formal_arg.rb +22 -0
- data/lib/mini_kraken/core/formal_ref.rb +24 -0
- data/lib/mini_kraken/core/goal.rb +9 -3
- data/lib/mini_kraken/core/goal_arg.rb +5 -3
- data/lib/mini_kraken/core/goal_template.rb +60 -0
- data/lib/mini_kraken/core/k_boolean.rb +31 -0
- data/lib/mini_kraken/core/outcome.rb +14 -0
- data/lib/mini_kraken/core/relation.rb +7 -0
- data/lib/mini_kraken/core/succeed.rb +17 -13
- data/lib/mini_kraken/core/vocabulary.rb +22 -4
- data/lib/mini_kraken/glue/fresh_env.rb +45 -3
- data/lib/mini_kraken/glue/run_star_expression.rb +43 -26
- data/lib/mini_kraken/version.rb +1 -1
- data/spec/core/conde_spec.rb +147 -0
- data/spec/core/conj2_spec.rb +8 -9
- data/spec/core/def_relation_spec.rb +96 -0
- data/spec/core/disj2_spec.rb +0 -45
- data/spec/core/duck_fiber_spec.rb +12 -1
- data/spec/core/equals_spec.rb +3 -3
- data/spec/core/goal_template_spec.rb +74 -0
- data/spec/core/k_boolean_spec.rb +107 -0
- data/spec/core/outcome_spec.rb +48 -0
- data/spec/core/vocabulary_spec.rb +6 -0
- data/spec/glue/fresh_env_spec.rb +27 -1
- data/spec/glue/run_star_expression_spec.rb +535 -18
- data/spec/mini_kraken_spec.rb +2 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/factory_methods.rb +15 -0
- 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
|
-
|
8
|
-
module
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
14
|
+
def initialize
|
15
|
+
super('succeed', '#s')
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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 #
|
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
|
13
|
-
# @param goal [Core::Goal]
|
14
|
-
def initialize(
|
15
|
-
|
16
|
-
|
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 =
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
67
|
+
new_tail
|
51
68
|
end
|
52
69
|
end # class
|
53
70
|
end # module
|
data/lib/mini_kraken/version.rb
CHANGED
@@ -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
|