porolog 0.0.4 → 1.0.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -5
  3. data/Rakefile +7 -2
  4. data/bin/porolog +58 -1
  5. data/coverage/badge.svg +1 -1
  6. data/coverage/index.html +76733 -2638
  7. data/doc/Array.html +1066 -0
  8. data/doc/Object.html +674 -0
  9. data/doc/Porolog.html +4153 -74
  10. data/doc/Symbol.html +501 -0
  11. data/doc/_index.html +280 -6
  12. data/doc/class_list.html +1 -1
  13. data/doc/file.README.html +34 -39
  14. data/doc/index.html +34 -39
  15. data/doc/method_list.html +1337 -57
  16. data/doc/top-level-namespace.html +4 -2
  17. data/lib/porolog.rb +1144 -4
  18. data/lib/porolog/arguments.rb +28 -24
  19. data/lib/porolog/core_ext.rb +188 -0
  20. data/lib/porolog/error.rb +9 -0
  21. data/lib/porolog/goal.rb +357 -0
  22. data/lib/porolog/instantiation.rb +346 -0
  23. data/lib/porolog/predicate.rb +74 -31
  24. data/lib/porolog/predicate/builtin.rb +825 -0
  25. data/lib/porolog/rule.rb +162 -0
  26. data/lib/porolog/scope.rb +4 -4
  27. data/lib/porolog/tail.rb +57 -0
  28. data/lib/porolog/value.rb +105 -0
  29. data/lib/porolog/variable.rb +325 -0
  30. data/test/porolog/arguments_test.rb +244 -195
  31. data/test/porolog/core_ext_test.rb +290 -0
  32. data/test/porolog/goal_test.rb +891 -0
  33. data/test/porolog/instantiation_test.rb +910 -0
  34. data/test/porolog/porolog_test.rb +2376 -13
  35. data/test/porolog/predicate/builtin_test.rb +1340 -0
  36. data/test/porolog/predicate_test.rb +84 -30
  37. data/test/porolog/rule_test.rb +527 -0
  38. data/test/porolog/scope_test.rb +0 -2
  39. data/test/porolog/tail_test.rb +127 -0
  40. data/test/porolog/value_test.rb +315 -0
  41. data/test/porolog/variable_test.rb +1614 -0
  42. data/test/samples_test.rb +277 -0
  43. data/test/test_helper.rb +115 -0
  44. metadata +34 -7
@@ -0,0 +1,346 @@
1
+ #
2
+ # lib/porolog/instantiation.rb - Plain Old Ruby Objects Prolog Engine -- Instantiation
3
+ #
4
+ # Luis Esteban 2 May 2018
5
+ # created
6
+ #
7
+
8
+ module Porolog
9
+
10
+ # A Porolog::Instantiation implements an instantiation of a Variable of a Goal.
11
+ # An Instantiation joins two expressions. At least one expression must have a variable.
12
+ # An index may be provided to index into the value of either expression, or both may have an index.
13
+ # Thus, an element of each list could be instantiated without reference to the other elements:
14
+ # x = [a,b,c,d,e]
15
+ # |
16
+ # y = [p,q,r,s]
17
+ #
18
+ # @author Luis Esteban
19
+ #
20
+ # @!attribute variable1
21
+ # @return [Variable,Object] one end of the instantiation.
22
+ # @!attribute variable2
23
+ # @return [Variable,Object] the other end of the instantiation.
24
+ # @!attribute index1
25
+ # @return [Object,nil] the index into the value of variable1.
26
+ # @!attribute index2
27
+ # @return [Object,nil] the index into the value of variable2.
28
+ #
29
+ class Instantiation
30
+
31
+ # Error class for rescuing or detecting any Instantiation error.
32
+ class Error < PorologError ; end
33
+ # Error class indicating that an instantiation was created without any Variables.
34
+ class NoVariableError < Error ; end
35
+ # Error class indicating that an index could not be used although requested.
36
+ class UnhandledIndexError < Error ; end
37
+
38
+ attr_accessor :variable1, :variable2, :index1, :index2
39
+
40
+ # Clears all instantiations.
41
+ # @return [Boolean] success
42
+ def self.reset
43
+ @@instantiations = {}
44
+ true
45
+ end
46
+
47
+ reset
48
+
49
+ # @return [Hash{Array => Porolog::Instantiation}] all Instantiations
50
+ def self.instantiations
51
+ @@instantiations
52
+ end
53
+
54
+ # Finds or creates a new instantiation. At least one of the variables must be a Variable.
55
+ # @param variable1 [Porolog::Variable,Porolog::Value,Object] one end of the instantiation to be made.
56
+ # @param variable2 [Porolog::Variable,Porolog::Value,Object] the other end of the instantiation to be made.
57
+ # @param index1 [Integer,Symbol,nil] index into the value of variable1.
58
+ # @param index2 [Integer,Symbol,nil] index into the value of variable2.
59
+ # @return [Porolog::Instantiation] the found or created Instantiation.
60
+ def self.new(variable1, index1, variable2, index2)
61
+ raise NoVariableError, "Cannot instantiate non-variables: #{variable1.inspect} and #{variable2.inspect}" unless variable1.is_a?(Variable) || variable2.is_a?(Variable)
62
+
63
+ variable1 = Value.new variable1, variable2.goal unless variable1.is_a?(Variable) || variable1.is_a?(Value)
64
+ variable2 = Value.new variable2, variable1.goal unless variable2.is_a?(Variable) || variable2.is_a?(Value)
65
+
66
+ instantiation = \
67
+ @@instantiations[[variable1, index1, variable2, index2]] ||
68
+ @@instantiations[[variable2, index2, variable1, index1]] ||
69
+ super
70
+
71
+ instantiation
72
+ end
73
+
74
+ # Initializes and registers a new Instantiation. At least one of the variables must be a Variable.
75
+ # @param variable1 [Porolog::Variable,Porolog::Value,Object] one end of the instantiation to be made.
76
+ # @param variable2 [Porolog::Variable,Porolog::Value,Object] the other end of the instantiation to be made.
77
+ # @param index1 [Integer,Symbol,nil] index into the value of variable1.
78
+ # @param index2 [Integer,Symbol,nil] index into the value of variable2.
79
+ # @return [Porolog::Instantiation] the found or created Instantiation.
80
+ def initialize(variable1, index1, variable2, index2)
81
+ @variable1 = variable1
82
+ @variable2 = variable2
83
+ @index1 = index1
84
+ @index2 = index2
85
+
86
+ @variable1.instantiations << self
87
+ @variable2.instantiations << self
88
+
89
+ @@instantiations[signature] = self
90
+ end
91
+
92
+ # @return [Array] the signature of the Instantiation, which aids in avoiding duplication instantiations.
93
+ def signature
94
+ @signature ||= [@variable1, @index1, @variable2, @index2].freeze
95
+ @signature
96
+ end
97
+
98
+ # @return [String] pretty representation.
99
+ def inspect(indent = 0)
100
+ index1 = @index1 ? "[#{@index1.inspect}]" : ''
101
+ index2 = @index2 ? "[#{@index2.inspect}]" : ''
102
+
103
+ "#{' ' * indent}#{@variable1.inspect}#{index1} = #{@variable2.inspect}#{index2}"
104
+ end
105
+
106
+ # Removes itself from its variables' Instantiations and unregisters itself.
107
+ # @return [Boolean] success
108
+ def remove
109
+ @variable1.instantiations.delete(self) if @variable1.is_a?(Variable) || @variable1.is_a?(Value)
110
+ @variable2.instantiations.delete(self) if @variable2.is_a?(Variable) || @variable2.is_a?(Value)
111
+
112
+ @deleted = true
113
+ @@instantiations.delete(signature)
114
+ @deleted
115
+ end
116
+
117
+ # Returns the Goal of the other Variable to the one provided.
118
+ # @param variable [Porolog::Variable] the provided Variable.
119
+ # @return [Porolog::Goal,nil] the Goal of the other Variable.
120
+ def other_goal_to(variable)
121
+ return nil unless variables.include?(variable)
122
+
123
+ other_variable = (variables - [variable]).first
124
+ other_variable&.goal
125
+ end
126
+
127
+ # @return [Array<Porolog::Goal>] the Goals of the Variables of the Instantiation.
128
+ def goals
129
+ [@variable1.goal,@variable2.goal]
130
+ end
131
+
132
+ # @return [Array<Porolog::Variable,Object>] the Variables of the Instantiation.
133
+ def variables
134
+ [@variable1,@variable2]
135
+ end
136
+
137
+ # @param visited [Array] prevents infinite recursion.
138
+ # @return [Array] the values of the Variables of the Instantiation.
139
+ def values(visited = [])
140
+ return [] if visited.include?(self)
141
+
142
+ values_for_variable1 = values_for(@variable1, visited)
143
+ values_for_variable2 = values_for(@variable2, visited)
144
+
145
+ (values_for_variable1 + values_for_variable2).uniq
146
+ end
147
+
148
+ # @param value [Object] the provided value.
149
+ # @param index [Integer,Symbol,Array<Integer>] the index.
150
+ # @param visited [Array] prevents infinite recursion.
151
+ # @return [Object] the indexed value of the provided value.
152
+ def value_indexed(value, index, visited = [])
153
+ return nil unless value
154
+ return nil if value.type == :variable
155
+ return nil if value == [] && index == :tail
156
+
157
+ visit = [value, index]
158
+ return nil if visited.include?(visit)
159
+ visited = visited + [visit]
160
+
161
+ case index
162
+ when Integer
163
+ if value.respond_to?(:last) && value.last == UNKNOWN_TAIL
164
+ if index >= value.length - 1
165
+ UNKNOWN_TAIL
166
+ else
167
+ value[index]
168
+ end
169
+ elsif value.is_a?(Numeric)
170
+ value
171
+ else
172
+ value[index]
173
+ end
174
+
175
+ when Symbol
176
+ value_value = value.value(visited)
177
+ if index == :flathead
178
+ # value_value = [..., 4, 5, 6]
179
+ if value_value.first == UNKNOWN_TAIL
180
+ UNKNOWN_ARRAY
181
+ else
182
+ nil
183
+ end
184
+ elsif index == :flattail
185
+ # value_value = [1, 2, 3, ...]
186
+ if value_value.first == UNKNOWN_TAIL
187
+ nil
188
+ elsif value_value.last == UNKNOWN_TAIL
189
+ UNKNOWN_ARRAY
190
+ end
191
+ elsif value_value.respond_to?(index)
192
+ value_value.send(index)
193
+ else
194
+ value
195
+ end
196
+
197
+ when Array
198
+ if index.empty?
199
+ value[1..-1]
200
+ else
201
+ value[0...index.first]
202
+ end
203
+
204
+ else
205
+ if index
206
+ raise UnhandledIndexError, "Unhandled index: #{index.inspect} of #{value.inspect}"
207
+ else
208
+ value
209
+ end
210
+ end
211
+ end
212
+
213
+ # @param variable [Porolog::Variable,Porolog::Value] the specified variable (or end of the Instantiation).
214
+ # @param visited [Array] prevents infinite recursion.
215
+ # @return [Array<Object>] the values for the specified variable by finding the values of the other variable (i.e. the other end) and applying any indexes.
216
+ def values_for(variable, visited = [])
217
+ return [] if visited.include?(self)
218
+ visited = visited + [self]
219
+
220
+ if variable == @variable1
221
+ if @index1 == :flathead
222
+ flathead = value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1).value.value
223
+ if flathead
224
+ return [[*flathead]]
225
+ else
226
+ return []
227
+ end
228
+ end
229
+ if @index1 == :flattail
230
+ flattail = value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1).value.value
231
+ if flattail
232
+ return [[UNKNOWN_TAIL, *flattail]]
233
+ else
234
+ return []
235
+ end
236
+ end
237
+ if @index1
238
+ [value_at_index(value_indexed(@variable2.value(visited), @index2, visited), @index1)]
239
+ else
240
+ if @variable2.is_a?(Variable)
241
+ [value_indexed(@variable2.value(visited), @index2, visited)]
242
+ else
243
+ [value_indexed(@variable2, @index2, visited)]
244
+ end
245
+ end
246
+ elsif variable == @variable2
247
+ if @index2 == :flathead
248
+ flathead = value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2).value.value
249
+ if flathead
250
+ return [[*flathead]]
251
+ else
252
+ return []
253
+ end
254
+ end
255
+ if @index2 == :flattail
256
+ flattail = value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2).value.value
257
+ if flattail
258
+ return [[UNKNOWN_TAIL, *flattail]]
259
+ else
260
+ return []
261
+ end
262
+ end
263
+ if @index2
264
+ [value_at_index(value_indexed(@variable1.value(visited), @index1, visited), @index2)]
265
+ else
266
+ if @variable1.is_a?(Variable)
267
+ [value_indexed(@variable1.value(visited), @index1, visited)]
268
+ else
269
+ [value_indexed(@variable1, @index1, visited)]
270
+ end
271
+ end
272
+ else
273
+ []
274
+ end.compact
275
+ end
276
+
277
+ # @param value [Object] the known value.
278
+ # @param index [Integer,Symbol,Array] the known index.
279
+ # @return [Array] an Array where the known value is at the known index.
280
+ def value_at_index(value, index)
281
+ value && case index
282
+ when Integer
283
+ result = []
284
+ result[index] = value
285
+ result << UNKNOWN_TAIL
286
+ result
287
+
288
+ when Symbol
289
+ case index
290
+ when :flathead
291
+ [*value, UNKNOWN_TAIL]
292
+ when :head
293
+ [value, UNKNOWN_TAIL]
294
+ when :tail
295
+ [nil, *value]
296
+ when :flattail
297
+ value
298
+ else
299
+ raise UnhandledIndexError, "Unhandled index: #{index.inspect} for #{value.inspect}"
300
+ end
301
+
302
+ when Array
303
+ if index.empty?
304
+ [nil, *value]
305
+ else
306
+ [value, UNKNOWN_TAIL]
307
+ end
308
+
309
+ else
310
+ if index
311
+ raise UnhandledIndexError, "Unhandled index: #{index.inspect} for #{value.inspect}"
312
+ else
313
+ value
314
+ end
315
+ end
316
+ end
317
+
318
+ # @param variable [Porolog::Variable,Porolog::Value] the specified variable.
319
+ # @return [Boolean] whether the specified variable is not indexed.
320
+ def without_index_on?(variable)
321
+ [
322
+ [@variable1,@index1],
323
+ [@variable2,@index2],
324
+ ].any?{|pair|
325
+ pair.first == variable && pair.last.nil?
326
+ }
327
+ end
328
+
329
+ # @return [Boolean] whether the Instantiation has been deleted (memoized).
330
+ def deleted?
331
+ @deleted ||= @variable1.goal.deleted? || @variable2.goal.deleted?
332
+ @deleted
333
+ end
334
+
335
+ # @param goal [Porolog::Goal] the provided Goal.
336
+ # @return [Boolean] whether the Instantiation attaches to a variable in the provided Goal.
337
+ def belongs_to?(goal)
338
+ [
339
+ @variable1.goal,
340
+ @variable2.goal,
341
+ ].include?(goal)
342
+ end
343
+
344
+ end
345
+
346
+ end
@@ -10,20 +10,28 @@ module Porolog
10
10
  # A Porolog::Predicate corresponds to a Prolog predicate.
11
11
  # It contains rules (Porolog::Rule) and belongs to a Porolog::Scope.
12
12
  # When provided with arguments, it produces a Porolog::Arguments.
13
+ #
13
14
  # @author Luis Esteban
15
+ #
14
16
  # @!attribute [r] name
15
17
  # Name of the Predicate.
18
+ #
16
19
  # @!attribute [r] rules
17
20
  # Rules of the Predicate.
21
+ #
18
22
  class Predicate
19
23
 
20
24
  # Error class for rescuing or detecting any Predicate error.
21
- class PredicateError < PorologError ; end
25
+ class Error < PorologError ; end
22
26
  # Error class indicating an error with the name of a Predicate.
23
- class NameError < PredicateError ; end
27
+ class NameError < Error ; end
24
28
 
25
29
  attr_reader :name, :rules
26
30
 
31
+ # A unique value used to verify instantiations.
32
+ UNIQUE_VALUE = Object.new.freeze
33
+ private_constant :UNIQUE_VALUE
34
+
27
35
  # Returns the current scope, or sets the current scope if a paramter is provided
28
36
  # (creating the new scope if necessary).
29
37
  # @param scope_name [Object] the name (or otherwise object) used to register a scope.
@@ -40,9 +48,7 @@ module Porolog
40
48
  # @param scope_name [Object] the name (or otherwise object) used to register a scope.
41
49
  # @return [Porolog::Scope] the current Scope.
42
50
  def self.scope=(scope_name)
43
- if scope_name
44
- @@current_scope = Scope[scope_name] || Scope.new(scope_name)
45
- end
51
+ @@current_scope = Scope[scope_name] || Scope.new(scope_name) if scope_name
46
52
  @@current_scope
47
53
  end
48
54
 
@@ -65,11 +71,12 @@ module Porolog
65
71
  # Initializes a Porolog::Predicate and registers it by its name.
66
72
  # @param name [#to_sym] the input object to read from
67
73
  # @param scope_name the name of the scope in which to register the Predicate; if omitted, defaults to the name of the current scope
68
- def initialize(name, scope_name = Predicate.scope.name)
69
- @name = name.to_sym
70
- @rules = []
74
+ def initialize(name, scope_name = Predicate.scope.name, builtin: false)
75
+ @name = name.to_sym
76
+ @rules = []
77
+ @builtin = builtin
71
78
 
72
- raise NameError.new("Cannot name a predicate 'predicate'") if @name == :predicate
79
+ raise NameError, "Cannot name a predicate 'predicate'" if @name == :predicate
73
80
 
74
81
  scope = Scope[scope_name] || Scope.new(scope_name)
75
82
  scope[@name] = self
@@ -78,21 +85,23 @@ module Porolog
78
85
  # Creates a new Predicate, or returns an existing Predicate if one already exists with the given name in the current scope.
79
86
  # @return [Porolog::Predicate] a new or existing Predicate.
80
87
  def self.new(*args)
81
- name, _ = *args
82
- scope[name.to_sym] || super
88
+ scope[args.first.to_sym] || super
83
89
  end
84
90
 
85
91
  # Create Arguments for the Predicate.
86
- # Provides the syntax options:
87
- # * p.(x,y,z)
92
+ # Provides the syntax option:
93
+ # p.(x,y,z)
94
+ # for
95
+ # p.arguments(x,y,z)
88
96
  # @return [Porolog::Arguments] Arguments of the Predicate with the given arguments.
89
- def call(*args)
90
- Arguments.new(self,args)
97
+ def call(*args, &block)
98
+ Arguments.new(self, args, &block)
91
99
  end
92
100
 
93
101
  # Create Arguments for the Predicate.
94
- def arguments(*args)
95
- Arguments.new(self,args)
102
+ # @return [Porolog::Arguments] Arguments of the Predicate with the given arguments.
103
+ def arguments(*args, &block)
104
+ Arguments.new(self, args, &block)
96
105
  end
97
106
 
98
107
  # Add a Rule to the Predicate.
@@ -106,28 +115,62 @@ module Porolog
106
115
  # A pretty print String of the Predicate.
107
116
  # @return [String] the Predicate as a String.
108
117
  def inspect
109
- lines = []
110
-
111
118
  if @rules.size == 1
112
- lines << "#{@name}:-#{@rules.first.inspect}"
119
+ "#{@name}:-#{@rules.first.inspect}"
113
120
  else
114
- lines << "#{@name}:-"
115
- @rules.each do |rule|
121
+ @rules.each_with_object(["#{@name}:-"]) do |rule, lines|
116
122
  lines << rule.inspect
123
+ end.join("\n")
124
+ end
125
+ end
126
+
127
+ # Satisfy the Predicate within the supplied Goal.
128
+ # Satisfy of each rule of the Predicate is called with the Goal and success block.
129
+ # @param goal [Porolog::Goal] the Goal within which to satisfy the Predicate.
130
+ # @param block [Proc] the block to be called each time a Rule of the Predicate is satisfied.
131
+ # @return [Boolean] whether any Rule was satisfied.
132
+ def satisfy(goal, &block)
133
+ if builtin?
134
+ satisfy_builtin(goal, &block)
135
+ else
136
+ satisfied = false
137
+ @rules.each do |rule|
138
+ rule.satisfy(goal) do |subgoal|
139
+ satisfied = true
140
+ block.call(subgoal)
141
+ end
142
+ break if goal.terminated?
117
143
  end
144
+ satisfied
118
145
  end
119
-
120
- lines.join("\n")
121
146
  end
122
147
 
123
- # Return a builtin Predicate based on its key.
124
- # @param key [Symbol] the name (or otherwise object) used to register a scope.
125
- # @return [Porolog::Predicate] a Predicate with the next id based on the key.
126
- def self.builtin(key)
127
- @builtin_predicate_ids[key] ||= 0
128
- @builtin_predicate_ids[key] += 1
148
+ # @return [Boolean] whether the Predicate is a builtin predicate.
149
+ def builtin?
150
+ @builtin
151
+ end
152
+
153
+ # Satisfy the builtin Predicate within the supplied Goal.
154
+ # Call the builtin Predicate method with the Goal and success block.
155
+ # The arguments and block are extracted from the Goal's Arguments.
156
+ # @param goal [Porolog::Goal] the Goal within which to satisfy the Predicate.
157
+ # @param block [Proc] the block to be called each time a Rule of the Predicate is satisfied.
158
+ # @return [Boolean] whether any Rule was satisfied.
159
+ def satisfy_builtin(goal, &block)
160
+ predicate = goal.arguments.predicate.name
161
+ arguments = goal.arguments.arguments
162
+ arg_block = goal.arguments.block
129
163
 
130
- self.new("_#{key}_#{@builtin_predicate_ids[key]}")
164
+ Predicate.call_builtin(predicate, goal, block, *arguments, &arg_block)
165
+ end
166
+
167
+ # Call a builtin predicate
168
+ # @param predicate [Symbol] the name of the predicate to call.
169
+ # @param args [Array<Object>] arguments for the predicate call.
170
+ # @param arg_block [Proc] the block provided in the Arguments of the Predicate.
171
+ # @return [Boolean] whether the predicate was satisfied.
172
+ def self.call_builtin(predicate, *args, &arg_block)
173
+ Porolog::Predicate::Builtin.instance_method(predicate).bind(self).call(*args, &arg_block)
131
174
  end
132
175
 
133
176
  end