porolog 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 +18 -4
- data/bin/porolog +38 -2
- data/coverage/badge.svg +1 -1
- data/coverage/index.html +52327 -6692
- data/doc/Array.html +113 -33
- data/doc/Object.html +11 -17
- data/doc/Porolog.html +3681 -73
- data/doc/Symbol.html +17 -20
- data/doc/_index.html +181 -13
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +28 -38
- data/doc/index.html +28 -38
- data/doc/method_list.html +871 -167
- data/doc/top-level-namespace.html +2 -2
- data/lib/porolog.rb +1015 -2
- data/lib/porolog/arguments.rb +16 -14
- data/lib/porolog/core_ext.rb +6 -0
- data/lib/porolog/error.rb +6 -0
- data/lib/porolog/goal.rb +205 -68
- data/lib/porolog/instantiation.rb +300 -0
- data/lib/porolog/predicate.rb +33 -16
- data/lib/porolog/rule.rb +129 -2
- data/lib/porolog/scope.rb +3 -3
- data/lib/porolog/tail.rb +52 -0
- data/lib/porolog/value.rb +9 -12
- data/lib/porolog/variable.rb +307 -0
- data/test/porolog/arguments_test.rb +217 -135
- data/test/porolog/core_ext_test.rb +24 -17
- data/test/porolog/goal_test.rb +481 -74
- data/test/porolog/instantiation_test.rb +874 -0
- data/test/porolog/porolog_test.rb +2121 -13
- data/test/porolog/predicate_test.rb +1 -1
- data/test/porolog/rule_test.rb +395 -0
- data/test/porolog/scope_test.rb +0 -2
- data/test/porolog/tail_test.rb +127 -0
- data/test/porolog/value_test.rb +1 -1
- data/test/porolog/variable_test.rb +1625 -0
- data/test/test_helper.rb +78 -5
- metadata +12 -4
data/lib/porolog/arguments.rb
CHANGED
@@ -11,7 +11,9 @@ module Porolog
|
|
11
11
|
# A predicate is not like a subroutine, although there are many similarities.
|
12
12
|
# An Arguments represents an instance of a predicate with a specific set of arguments.
|
13
13
|
# This forms the basis for implementing a goal to solve the predicate with those specific arguments.
|
14
|
+
#
|
14
15
|
# @author Luis Esteban
|
16
|
+
#
|
15
17
|
# @!attribute [r] predicate
|
16
18
|
# The Predicate for which these are the arguments.
|
17
19
|
# @!attribute [r] arguments
|
@@ -52,7 +54,7 @@ module Porolog
|
|
52
54
|
|
53
55
|
# @return [String] pretty representation
|
54
56
|
def inspect
|
55
|
-
"#{@predicate.name}(#{@arguments && @arguments.map(&:inspect).join(',')})"
|
57
|
+
"#{@predicate && @predicate.name}(#{@arguments && @arguments.map(&:inspect).join(',')})"
|
56
58
|
end
|
57
59
|
|
58
60
|
# Creates a fact rule that states that these arguments satisfy the Predicate.
|
@@ -114,18 +116,18 @@ module Porolog
|
|
114
116
|
end
|
115
117
|
|
116
118
|
# Returns memoized solutions
|
117
|
-
# @param
|
119
|
+
# @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
|
118
120
|
# @return [Array<Hash{Symbol => Object}>] the solutions found (memoized)
|
119
|
-
def solutions(
|
120
|
-
@solutions ||= solve(
|
121
|
+
def solutions(max_solutions = nil)
|
122
|
+
@solutions ||= solve(max_solutions)
|
121
123
|
@solutions
|
122
124
|
end
|
123
125
|
|
124
126
|
# Solves the Arguments
|
125
|
-
# @param
|
127
|
+
# @param max_solutions [Integer] the maximum number of solutions to find (nil means find all)
|
126
128
|
# @return [Array<Hash{Symbol => Object}>] the solutions found
|
127
|
-
def solve(
|
128
|
-
@solutions = goal.solve(
|
129
|
+
def solve(max_solutions = nil)
|
130
|
+
@solutions = goal.solve(max_solutions)
|
129
131
|
end
|
130
132
|
|
131
133
|
# Extracts solution values.
|
@@ -141,7 +143,12 @@ module Porolog
|
|
141
143
|
def solve_for(*variables)
|
142
144
|
variables = [*variables]
|
143
145
|
solutions.map{|solution|
|
144
|
-
variables.map{|variable| solution[variable] }
|
146
|
+
values = variables.map{|variable| solution[variable] }
|
147
|
+
if values.size == 1
|
148
|
+
values.first
|
149
|
+
else
|
150
|
+
values
|
151
|
+
end
|
145
152
|
}
|
146
153
|
end
|
147
154
|
|
@@ -154,12 +161,7 @@ module Porolog
|
|
154
161
|
# @param goal [Porolog::Goal] the destination goal
|
155
162
|
# @return [Porolog::Arguments] the duplicated Arguments
|
156
163
|
def dup(goal)
|
157
|
-
|
158
|
-
# TODO: Uncomment when HeadTail added
|
159
|
-
#(0...arguments_dup.size).each do |i|
|
160
|
-
# arguments_dup[i] = arguments_dup[i].dup(goal) if arguments_dup[i].is_a?(HeadTail)
|
161
|
-
#end
|
162
|
-
self.class.new @predicate, arguments_dup
|
164
|
+
self.class.new @predicate, goal.variablise(arguments)
|
163
165
|
end
|
164
166
|
|
165
167
|
# @param other [Porolog::Arguments] arguments for comparison
|
data/lib/porolog/core_ext.rb
CHANGED
@@ -41,6 +41,7 @@ class Object
|
|
41
41
|
[self]/other
|
42
42
|
end
|
43
43
|
|
44
|
+
# Syntactic sugar to look like Prolog's [H|T]
|
44
45
|
alias / :tail
|
45
46
|
|
46
47
|
# @return [Boolean] whether the Object is an Array with a head and a tail (for an Object, should be false).
|
@@ -161,4 +162,9 @@ class Array
|
|
161
162
|
length == 2 && (last.is_a?(Tail) || last == UNKNOWN_TAIL)
|
162
163
|
end
|
163
164
|
|
165
|
+
# @return [Porolog::Goal] the goal that is most likely to be the goal for this array.
|
166
|
+
def goal
|
167
|
+
map{|element| element.goal if element.respond_to?(:goal) }.flatten.find{|goal| goal.is_a?(Porolog::Goal) }
|
168
|
+
end
|
169
|
+
|
164
170
|
end
|
data/lib/porolog/error.rb
CHANGED
@@ -8,6 +8,12 @@
|
|
8
8
|
module Porolog
|
9
9
|
|
10
10
|
# Error class to enable rescuing or detecting any Porolog error.
|
11
|
+
#
|
12
|
+
# @author Luis Esteban
|
13
|
+
#
|
11
14
|
class PorologError < RuntimeError ; end
|
15
|
+
|
16
|
+
# Error indicating that a Goal is needed but could not be found.
|
17
|
+
class NoGoalError < PorologError ; end
|
12
18
|
|
13
19
|
end
|
data/lib/porolog/goal.rb
CHANGED
@@ -8,60 +8,85 @@
|
|
8
8
|
module Porolog
|
9
9
|
|
10
10
|
# A Porolog::Goal finds solutions for specific Arguments of a Predicate.
|
11
|
+
#
|
11
12
|
# @author Luis Esteban
|
13
|
+
#
|
12
14
|
# @!attribute calling_goal
|
13
|
-
# @return [Porolog::Goal] The parent goal if this goal is a subgoal.
|
15
|
+
# @return [Porolog::Goal, nil] The parent goal if this goal is a subgoal, otherwise nil.
|
14
16
|
# @!attribute arguments
|
15
17
|
# @return [Porolog::Arguments] The specific Arguments this goal is trying to solve.
|
16
|
-
# @!attribute terminate
|
17
|
-
# @return [Boolean] Whether this Goal is to terminate.
|
18
18
|
# @!attribute index
|
19
|
-
# @return [Integer,
|
19
|
+
# @return [Integer, nil] Solution index for builtin predicates.
|
20
20
|
# @!attribute result
|
21
21
|
# @return [Boolean] Whether any result has been found.
|
22
|
+
# @!attribute description
|
23
|
+
# @return [String] Description of the Arguments the Goal is attempting to solve.
|
24
|
+
# @!attribute log
|
25
|
+
# @return [Array<String>] Log of events, namely unification failures.
|
26
|
+
#
|
22
27
|
class Goal
|
23
28
|
|
24
29
|
# Error class for rescuing or detecting any Goal error.
|
25
|
-
class
|
30
|
+
class Error < PorologError ; end
|
26
31
|
# Error class indicating an unexpected type of Rule definition.
|
27
|
-
class DefinitionError
|
32
|
+
class DefinitionError < Error ; end
|
28
33
|
# Error class indicating a non-Variable was attempted to be instantiated.
|
29
|
-
class NotVariableError
|
34
|
+
class NotVariableError < Error ; end
|
30
35
|
|
31
36
|
# Clears all goals and ensures they are deleted.
|
32
|
-
# @return [
|
37
|
+
# @return [Boolean] success
|
33
38
|
def self.reset
|
34
39
|
@@goals ||= []
|
35
40
|
goals = @@goals
|
36
41
|
@@goals = []
|
37
42
|
goals.map(&:deleted?)
|
38
|
-
@@cache = {}
|
39
43
|
true
|
40
44
|
end
|
41
45
|
|
42
46
|
reset
|
43
47
|
|
44
|
-
attr_accessor :calling_goal, :arguments, :
|
48
|
+
attr_accessor :calling_goal, :arguments, :index, :result, :description, :log
|
45
49
|
|
46
50
|
# Initializes and registers the Goal.
|
47
51
|
# @param arguments [Porolog::Arguments] the Predicate Arguments that this Goal is to solve.
|
48
52
|
# @param calling_goal [Porolog::Goal] the parent Goal if this Goal is a subgoal.
|
49
53
|
def initialize(arguments, calling_goal = nil)
|
54
|
+
@@goals << self
|
55
|
+
|
50
56
|
@arguments = arguments
|
51
57
|
@terminate = false
|
52
58
|
@calling_goal = calling_goal
|
53
59
|
@variables = {}
|
54
|
-
@
|
60
|
+
@values = {}
|
61
|
+
@result = nil
|
62
|
+
@description = "Solve #{arguments.inspect}"
|
63
|
+
@log = []
|
55
64
|
|
56
65
|
@arguments = @arguments.dup(self) if @arguments.is_a?(Arguments)
|
66
|
+
@solutions = []
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [Array<Porolog::Goal>] the calling chain for this goal.
|
70
|
+
def ancestors
|
71
|
+
ancestors = []
|
72
|
+
goal = self
|
57
73
|
|
58
|
-
|
74
|
+
while goal
|
75
|
+
ancestors << goal
|
76
|
+
goal = goal.calling_goal
|
77
|
+
end
|
78
|
+
|
79
|
+
ancestors.reverse
|
59
80
|
end
|
60
81
|
|
61
82
|
# A convenience method for testing/debugging.
|
62
|
-
# @return [
|
63
|
-
def
|
64
|
-
|
83
|
+
# @return [String] the calling chain for this goal as a String.
|
84
|
+
def ancestry
|
85
|
+
indent = -1
|
86
|
+
ancestors.map do |goal|
|
87
|
+
indent += 1
|
88
|
+
"#{' ' * indent}#{goal.myid} -- #{goal.description} #{goal.variables.inspect}"
|
89
|
+
end.join("\n")
|
65
90
|
end
|
66
91
|
|
67
92
|
# A convenience method for testing/debugging.
|
@@ -70,42 +95,125 @@ module Porolog
|
|
70
95
|
"Goal#{(@@goals.index(self) || -1000) + 1}"
|
71
96
|
end
|
72
97
|
|
98
|
+
# @return [String] pretty representation.
|
99
|
+
def inspect
|
100
|
+
"#{myid} -- #{description}"
|
101
|
+
end
|
102
|
+
|
103
|
+
# A convenience method for testing/debugging.
|
104
|
+
# @return [Array<Porolog::Goal>]
|
105
|
+
def self.goals
|
106
|
+
@@goals
|
107
|
+
end
|
108
|
+
|
109
|
+
# Delete the Goal
|
110
|
+
# @return [Boolean] whether the Goal has been deleted (memoized)
|
111
|
+
def delete!
|
112
|
+
@@goals.delete(self)
|
113
|
+
deleted?
|
114
|
+
end
|
115
|
+
|
73
116
|
# @return [Boolean] whether the Goal has been deleted (memoized)
|
74
117
|
def deleted?
|
75
118
|
@deleted ||= check_deleted
|
76
119
|
@deleted
|
77
120
|
end
|
78
121
|
|
79
|
-
# Determines whether the Goal has been deleted and removes its Variables if it has
|
122
|
+
# Determines whether the Goal has been deleted and removes its Variables and Values if it has.
|
80
123
|
# @return [Boolean] whether the Goal has been deleted
|
81
124
|
def check_deleted
|
82
|
-
if @@goals.include?(self)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
variable.remove
|
87
|
-
true
|
88
|
-
end
|
125
|
+
return false if @@goals.include?(self)
|
126
|
+
|
127
|
+
@variables.delete_if do |name,variable|
|
128
|
+
variable.remove
|
89
129
|
true
|
90
130
|
end
|
131
|
+
|
132
|
+
@values.delete_if do |name,value|
|
133
|
+
value.remove
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
# Sets that the goal should no longer search any further rules for the current predicate
|
141
|
+
# once the current rule has finished being evaluated.
|
142
|
+
# This method implements the :CUT rule.
|
143
|
+
# That is, backtracking does not go back past the point of the cut.
|
144
|
+
# @return [Boolean] true.
|
145
|
+
def terminate!
|
146
|
+
@log << 'terminating'
|
147
|
+
@terminate = true
|
148
|
+
end
|
149
|
+
|
150
|
+
# @return [Boolean] whether the goal has been cut.
|
151
|
+
def terminated?
|
152
|
+
@terminate
|
153
|
+
end
|
154
|
+
|
155
|
+
# Converts all embedded Symbols to Variables within this Goal.
|
156
|
+
# @param object [Object] the object to variablise.
|
157
|
+
# @return [Object] the variablised object.
|
158
|
+
def variablise(object)
|
159
|
+
case object
|
160
|
+
when Symbol,Variable
|
161
|
+
variable(object)
|
162
|
+
when Array
|
163
|
+
object.map{|element| variablise(element) }
|
164
|
+
when Arguments
|
165
|
+
object.dup(self)
|
166
|
+
when Tail
|
167
|
+
Tail.new variablise(object.value)
|
168
|
+
when Value
|
169
|
+
object
|
170
|
+
when NilClass
|
171
|
+
nil
|
172
|
+
else
|
173
|
+
if object == UNKNOWN_TAIL || object == UNKNOWN_ARRAY
|
174
|
+
object
|
175
|
+
else
|
176
|
+
value(object)
|
177
|
+
end
|
178
|
+
end
|
91
179
|
end
|
92
180
|
|
181
|
+
alias_method :[], :variablise
|
182
|
+
|
93
183
|
# @return [Hash{Symbol => Object}] the Variables and their current values of this Goal
|
94
184
|
def variables
|
95
185
|
@variables.keys.each_with_object({}){|variable,variable_list|
|
96
|
-
|
97
|
-
|
186
|
+
value = value_of(variable)
|
187
|
+
value = value.value if value.is_a?(Value)
|
188
|
+
if value.is_a?(Variable)
|
189
|
+
variable_list[variable] = nil
|
190
|
+
elsif value.is_a?(Value) || value.is_a?(Array)
|
191
|
+
variable_list[variable] = value.value
|
192
|
+
else
|
193
|
+
variable_list[variable] = value
|
194
|
+
end
|
98
195
|
}
|
99
196
|
end
|
100
197
|
|
198
|
+
# A convenience method for testing/debugging.
|
199
|
+
# @return [String] a tree representation of all the instantiations of this goal's variables.
|
200
|
+
def inspect_variables
|
201
|
+
@variables.map{|name,variable|
|
202
|
+
variable.inspect_with_instantiations
|
203
|
+
}.join("\n")
|
204
|
+
end
|
205
|
+
|
206
|
+
# A convenience method for testing/debugging.
|
207
|
+
# @return [Array<Object>] the values instantiated for this goal.
|
208
|
+
def values
|
209
|
+
@values.map{|name,value| value.value }
|
210
|
+
end
|
211
|
+
|
101
212
|
# Finds or tries to create a variable in the goal (as much as possible) otherwise passes the parameter back.
|
102
213
|
# @param name [Object] name of variable or variable
|
103
214
|
# @return [Porolog::Variable, Object] a variable of the goal or the original parameter
|
104
215
|
def variable(name)
|
105
|
-
unless name.is_a?(Symbol) || name.is_a?(Variable)
|
106
|
-
# -- Return parameter if it is not a variable --
|
107
|
-
return name
|
108
|
-
end
|
216
|
+
return name unless name.is_a?(Symbol) || name.is_a?(Variable)
|
109
217
|
|
110
218
|
if name.is_a?(Variable)
|
111
219
|
variable = name
|
@@ -117,60 +225,89 @@ module Porolog
|
|
117
225
|
@variables[name]
|
118
226
|
end
|
119
227
|
|
228
|
+
# @param value [Object] the value that needs to be associated with this Goal.
|
229
|
+
# @return [Porolog::Value] a Value, ensuring it has been associated with this Goal so that it can be uninstantiated.
|
230
|
+
def value(value)
|
231
|
+
@values[value] ||= Value.new(value, self)
|
232
|
+
@values[value]
|
233
|
+
end
|
234
|
+
|
120
235
|
# Returns the value of the indicated variable or value
|
121
236
|
# @param name [Symbol, Object] name of the variable or value
|
122
|
-
# @param
|
237
|
+
# @param index [Object] index is not yet used
|
123
238
|
# @param visited [Array] used to avoid infinite recursion
|
124
239
|
# @return [Object] the value
|
125
|
-
def value_of(name,
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
240
|
+
def value_of(name, index = nil, visited = [])
|
241
|
+
variable(name).value(visited)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns the values of an Object. For most objects, this will basically just be the object.
|
245
|
+
# For Arrays, an Array will be returned where the elements are the value of the elements.
|
246
|
+
# @param object [Object] the Object to find the values of.
|
247
|
+
# @return [Object,Array<Object>] the value(s) of the object.
|
248
|
+
def values_of(object)
|
249
|
+
case object
|
250
|
+
when Array
|
251
|
+
object.map{|element| values_of(element) }
|
252
|
+
when Variable,Symbol,Value
|
253
|
+
value_of(object)
|
254
|
+
when Tail
|
255
|
+
# TODO: This needs to splat outwards; in the meantime, it splats just the first.
|
256
|
+
value_of(object.value).first
|
257
|
+
else
|
258
|
+
object
|
139
259
|
end
|
140
|
-
|
141
|
-
value
|
142
260
|
end
|
143
261
|
|
144
262
|
# Finds all possible solutions to the Predicate for the specific Arguments.
|
145
|
-
# @param
|
263
|
+
# @param max_solutions [Integer] if provided, the search is stopped once the number of solutions has been found
|
146
264
|
# @return [Array<Hash{Symbol => Object}>]
|
147
|
-
def solve(
|
148
|
-
solutions
|
149
|
-
variables = @arguments.variables
|
265
|
+
def solve(max_solutions = nil)
|
266
|
+
return @solutions unless @arguments
|
150
267
|
|
151
|
-
|
152
|
-
self.variable(variable).internal?
|
153
|
-
}
|
268
|
+
predicate = @arguments.predicate
|
154
269
|
|
155
|
-
satisfy do |goal|
|
156
|
-
@
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
solution[variable.to_sym] = goal.value_of(variable)
|
161
|
-
}
|
270
|
+
predicate && predicate.satisfy(self) do |goal|
|
271
|
+
@solutions << variables
|
272
|
+
@log << "SOLUTION: #{variables}"
|
273
|
+
@log << goal.ancestry
|
274
|
+
@log << goal.inspect_variables
|
162
275
|
|
163
|
-
solutions
|
164
|
-
|
165
|
-
if number_of_solutions && solutions.length >= number_of_solutions
|
166
|
-
@solutions = solutions
|
167
|
-
return @solutions
|
168
|
-
end
|
276
|
+
return @solutions if max_solutions && @solutions.size >= max_solutions
|
169
277
|
end
|
170
278
|
|
171
|
-
|
279
|
+
@solutions
|
280
|
+
end
|
281
|
+
|
282
|
+
# Solves the goal but not as the root goal for a query.
|
283
|
+
# That is, solves an intermediate goal.
|
284
|
+
# @param block [Proc] code to perform when the Goal is satisfied.
|
285
|
+
# @return [Boolean] whether the definition was satisfied.
|
286
|
+
def satisfy(&block)
|
287
|
+
return false unless @arguments
|
288
|
+
|
289
|
+
predicate = @arguments.predicate
|
290
|
+
|
291
|
+
predicate && predicate.satisfy(self) do |subgoal|
|
292
|
+
block.call(subgoal)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Instantiates a Variable to another Variable or Value, for this Goal.
|
297
|
+
# @param name [Symbol,Porolog::Variable,Object] the name that references the variable to be instantiated.
|
298
|
+
# @param other [Object] the value that the variable is to be instantiated to.
|
299
|
+
# @param other_goal [Porolog::Goal,nil] the Goal of the other value.
|
300
|
+
# @return [Porolog::Instantiation] the instantiation.
|
301
|
+
def instantiate(name, other, other_goal = self)
|
302
|
+
variable = self.variable(name)
|
303
|
+
|
304
|
+
other = if other.type == :variable
|
305
|
+
other_goal.variable(other)
|
306
|
+
else
|
307
|
+
other_goal.value(other)
|
308
|
+
end
|
172
309
|
|
173
|
-
|
310
|
+
variable.instantiate(other)
|
174
311
|
end
|
175
312
|
|
176
313
|
end
|