mini_kraken 0.1.03 → 0.1.08

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -1
  3. data/CHANGELOG.md +54 -3
  4. data/Gemfile +3 -1
  5. data/README.md +22 -1
  6. data/Rakefile +5 -3
  7. data/lib/mini_kraken.rb +3 -1
  8. data/lib/mini_kraken/core/any_value.rb +9 -7
  9. data/lib/mini_kraken/core/association.rb +20 -7
  10. data/lib/mini_kraken/core/association_walker.rb +5 -1
  11. data/lib/mini_kraken/core/atomic_term.rb +5 -3
  12. data/lib/mini_kraken/core/binary_relation.rb +8 -6
  13. data/lib/mini_kraken/core/composite_goal.rb +46 -0
  14. data/lib/mini_kraken/core/composite_term.rb +7 -20
  15. data/lib/mini_kraken/core/conj2.rb +77 -0
  16. data/lib/mini_kraken/core/cons_cell.rb +51 -41
  17. data/lib/mini_kraken/core/designation.rb +55 -0
  18. data/lib/mini_kraken/core/disj2.rb +71 -0
  19. data/lib/mini_kraken/core/duck_fiber.rb +4 -2
  20. data/lib/mini_kraken/core/environment.rb +25 -11
  21. data/lib/mini_kraken/core/equals.rb +128 -189
  22. data/lib/mini_kraken/core/fail.rb +20 -14
  23. data/lib/mini_kraken/core/freshness.rb +11 -8
  24. data/lib/mini_kraken/core/goal.rb +8 -4
  25. data/lib/mini_kraken/core/goal_arg.rb +10 -0
  26. data/lib/mini_kraken/core/goal_relation.rb +28 -0
  27. data/lib/mini_kraken/core/k_integer.rb +4 -3
  28. data/lib/mini_kraken/core/k_symbol.rb +4 -3
  29. data/lib/mini_kraken/core/nullary_relation.rb +3 -1
  30. data/lib/mini_kraken/core/outcome.rb +29 -25
  31. data/lib/mini_kraken/core/relation.rb +4 -18
  32. data/lib/mini_kraken/core/succeed.rb +20 -14
  33. data/lib/mini_kraken/core/term.rb +7 -2
  34. data/lib/mini_kraken/core/variable.rb +11 -25
  35. data/lib/mini_kraken/core/variable_ref.rb +12 -59
  36. data/lib/mini_kraken/core/vocabulary.rb +267 -48
  37. data/lib/mini_kraken/glue/fresh_env.rb +5 -3
  38. data/lib/mini_kraken/glue/run_star_expression.rb +18 -8
  39. data/lib/mini_kraken/version.rb +3 -1
  40. data/mini_kraken.gemspec +15 -13
  41. data/spec/core/association_spec.rb +4 -4
  42. data/spec/core/association_walker_spec.rb +25 -24
  43. data/spec/core/conj2_spec.rb +114 -0
  44. data/spec/core/cons_cell_spec.rb +12 -3
  45. data/spec/core/disj2_spec.rb +99 -0
  46. data/spec/core/duck_fiber_spec.rb +22 -12
  47. data/spec/core/environment_spec.rb +16 -28
  48. data/spec/core/equals_spec.rb +7 -7
  49. data/spec/core/fail_spec.rb +7 -7
  50. data/spec/core/goal_spec.rb +10 -10
  51. data/spec/core/k_symbol_spec.rb +5 -6
  52. data/spec/core/succeed_spec.rb +4 -4
  53. data/spec/core/variable_ref_spec.rb +0 -4
  54. data/spec/core/vocabulary_spec.rb +33 -27
  55. data/spec/glue/fresh_env_spec.rb +1 -1
  56. data/spec/glue/run_star_expression_spec.rb +213 -60
  57. data/spec/mini_kraken_spec.rb +4 -0
  58. data/spec/spec_helper.rb +3 -2
  59. data/spec/support/factory_methods.rb +20 -2
  60. metadata +12 -2
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require_relative 'association'
1
5
  require_relative 'association_walker'
2
6
 
3
7
  module MiniKraken
@@ -19,31 +23,34 @@ module MiniKraken
19
23
  @rankings = {} unless aParent
20
24
  end
21
25
 
22
- # Return a Fiber object that can iterate over this vocabulary and
26
+ # Return a Enumerator object that can iterate over this vocabulary and
23
27
  # all its direct and indirect parent(s).
24
- # @return [Fiber<Vocabulary, NilClass>]
28
+ # @return [Enumerator<Vocabulary, NilClass>]
25
29
  def ancestor_walker
26
- Fiber.new do
30
+ unless @ancestors # Not yet in cache?...
31
+ @ancestors = []
27
32
  relative = self
28
- while relative do
29
- Fiber.yield relative
33
+ while relative
34
+ @ancestors << relative
30
35
  relative = relative.parent
31
36
  end
32
-
33
- Fiber.yield nil # nil marks end of iteration...
37
+ @ancestors << nil # nil marks end of iteration...
34
38
  end
39
+
40
+ @ancestors.to_enum
35
41
  end
36
42
 
37
43
  def clear_rankings
38
44
  walker = ancestor_walker
39
45
  orphan = nil
40
46
  loop do
41
- orphan_temp = walker.resume
47
+ orphan_temp = walker.next
42
48
  break unless orphan_temp
49
+
43
50
  orphan = orphan_temp
44
51
  end
45
52
 
46
- orphan.rankings.clear if orphan.rankings
53
+ orphan.rankings&.clear
47
54
  end
48
55
 
49
56
  # @param aName [String]
@@ -52,42 +59,50 @@ module MiniKraken
52
59
  walker = ancestor_walker
53
60
  orphan = nil
54
61
  loop do
55
- orphan_temp = walker.resume
62
+ orphan_temp = walker.next
56
63
  break unless orphan_temp
64
+
57
65
  orphan = orphan_temp
58
66
  end
59
67
 
60
68
  raise StandardError unless orphan
61
69
 
70
+ rank = nil
62
71
  if orphan.rankings.include?(aName)
63
- orphan.rankings[aName]
72
+ rank = orphan.rankings[aName]
64
73
  else
65
- other = alternate_names.find do |a_name|
66
- orphan.rankings.include?(a_name)
74
+ other = alternate_names.find do |a_name|
75
+ rank = orphan.rankings.include?(a_name)
67
76
  end
68
77
  if other
69
- get_rank(other)
78
+ rank = get_rank(other)
70
79
  else
71
80
  rank = orphan.rankings.keys.size
72
81
  orphan.rankings[aName] = rank
73
- rank
74
82
  end
75
83
  end
84
+
85
+ rank
76
86
  end
77
87
 
78
- # Record an association between a variable with given name and a term.
79
- # @param anAssociation [Association]
80
- def add_assoc(anAssociation)
81
- name = anAssociation.var_name
82
- unless include?(name)
88
+ # Record an association between a variable with given user-defined name
89
+ # and a term.
90
+ # @param aName [String, Variable] A user-defined variable name
91
+ # @param aTerm [Term] A term to associate with the variable
92
+ def add_assoc(aName, aTerm)
93
+ name = aName.respond_to?(:name) ? aName.name : aName
94
+
95
+ var = name2var(name)
96
+ unless var
83
97
  err_msg = "Unknown variable '#{name}'."
84
98
  raise StandardError, err_msg
85
99
  end
86
- found_assocs = associations[name]
87
- if found_assocs
88
- found_assocs << anAssociation
100
+ siblings = detect_fuse(var, aTerm)
101
+ if siblings.empty?
102
+ anAssociation = Association.new(var.i_name, aTerm)
103
+ do_add_assocs([anAssociation]).first
89
104
  else
90
- associations[name] = [anAssociation]
105
+ fuse_vars(siblings << var)
91
106
  end
92
107
  end
93
108
 
@@ -95,19 +110,117 @@ module MiniKraken
95
110
  # Can be overridden in other to propagate associations from child
96
111
  # @param _descendent [Outcome]
97
112
  def propagate(_descendent)
98
- #Do nothing...
113
+ # Do nothing...
99
114
  end
100
115
 
101
- # Remove all the associations.
116
+ # Remove all the associations of this vocabulary
102
117
  def clear
103
118
  associations.clear
104
119
  end
105
120
 
121
+ # @param aVarName [String] A user-defined variable name
122
+ # @param other [Vocabulary]
123
+ def move_assocs(aVarName, other)
124
+ i_name = to_internal(aVarName)
125
+ assocs = other.associations[i_name]
126
+ if assocs
127
+ do_add_assocs(assocs)
128
+ other.associations.delete(i_name)
129
+ end
130
+ end
131
+
106
132
  # Merge the associations from another vocabulary-like object.
107
133
  # @param another [Vocabulary]
108
134
  def merge(another)
109
- another.associations.each_pair do |_name, assocs|
110
- assocs.each { |a| add_assoc(a) }
135
+ another.associations.each_value { |assocs| do_add_assocs(assocs) }
136
+ end
137
+
138
+ # Check that the provided variable must be fused with the argument.
139
+ # @return [Array<Variable>]
140
+ def detect_fuse(aVariable, aTerm)
141
+ return [] unless aTerm.kind_of?(VariableRef)
142
+
143
+ assocs = self[aTerm.var_name]
144
+ # Simplified implementation: cope with binary cycles only...
145
+ # TODO: Extend to n-ary (n > 2) cycles
146
+ assoc_refs = assocs.select { |a| a.value.kind_of?(VariableRef) }
147
+ return [] if assoc_refs.empty? # No relevant association...
148
+
149
+ visitees = Set.new
150
+ to_fuse = []
151
+ to_visit = assoc_refs
152
+ loop do
153
+ assc = to_visit.shift
154
+ next if visitees.include?(assc)
155
+
156
+ visitees.add(assc)
157
+ ref = assc.value
158
+ if ref.var_name == aVariable.name
159
+ to_fuse << assc.i_name unless assc.i_name == aVariable.i_name
160
+ end
161
+ other_assocs = self[ref.var_name]
162
+ other_assoc_refs = other_assocs.select { |a| a.value.kind_of?(VariableRef) }
163
+ other_assoc_refs.each do |a|
164
+ to_visit << a unless visitess.include?(a)
165
+ end
166
+
167
+
168
+ break if to_visit.empty?
169
+ end
170
+
171
+ to_fuse.map { |i_name| i_name2var(i_name) }
172
+ end
173
+
174
+ # Fuse the given variables, that is:
175
+ # Collect all their associations
176
+ # Put them under a new internal name
177
+ # Remove all entries from old internal names
178
+ # For all fused variables, change internal names
179
+ # @param theVars [Array<Variable>]
180
+ def fuse_vars(theVars)
181
+ new_i_name = Object.new.object_id.to_s
182
+ fused_vars = theVars.dup
183
+ fused_vars.each do |a_var|
184
+ old_i_name = a_var.i_name
185
+ old_names = fused_vars.map(&:name)
186
+ walker = ancestor_walker
187
+
188
+ loop do
189
+ voc = walker.next
190
+ break unless voc
191
+
192
+ if voc.associations.include?(old_i_name)
193
+ assocs = voc.associations[old_i_name]
194
+ keep_assocs = assocs.reject do |assc|
195
+ assc.value.kind_of?(VariableRef) && old_names.include?(assc.value.var_name)
196
+ end
197
+ unless keep_assocs.empty?
198
+ keep_assocs.each { |assc| assc.i_name = new_i_name }
199
+ if voc.associations.include?(new_i_name)
200
+ voc.associations[new_i_name].concat(keep_assocs)
201
+ else
202
+ voc.associations[new_i_name] = keep_assocs
203
+ end
204
+ end
205
+ voc.associations.delete(old_i_name)
206
+ end
207
+ next unless voc.respond_to?(:vars) && voc.vars.include?(a_var.name)
208
+
209
+ user_names = voc.ivars[old_i_name]
210
+ unseen = user_names.reject { |nm| old_names.include?(nm) }
211
+ unseen.each do |usr_name|
212
+ new_var = name2var(usr_name)
213
+ fused_vars << new_var
214
+ end
215
+ unless voc.ivars.include?(new_i_name)
216
+ voc.ivars[new_i_name] = user_names
217
+ else
218
+ voc.ivars[new_i_name].merge(user_names)
219
+ end
220
+ voc.ivars.delete(old_i_name)
221
+ break
222
+ end
223
+ a_var.i_name = new_i_name
111
224
  end
112
225
  end
113
226
 
@@ -119,7 +232,7 @@ module MiniKraken
119
232
  end
120
233
 
121
234
  # @param var [Variable, VariableRef] variable for which the value to retrieve
122
- # @return [Term, NilClase]
235
+ # @return [Term, NilClass]
123
236
  def ground_value(var)
124
237
  name = var.respond_to?(:var_name) ? var.var_name : var.name
125
238
 
@@ -127,7 +240,7 @@ module MiniKraken
127
240
  walker.find_ground(name, self)
128
241
  end
129
242
 
130
- # @param var [CompositeTerm] the composite term to check.
243
+ # @param val [CompositeTerm] the composite term to check.
131
244
  # @return [Boolean]
132
245
  def fresh_value?(val)
133
246
  walker = AssociationWalker.new
@@ -149,7 +262,7 @@ module MiniKraken
149
262
  # Determine whether the reference points to a fresh, bound or ground term.
150
263
  # @param aVariableRef [VariableRef]
151
264
  # @return [Freshness]
152
- def freshness_ref(aVariableRef)
265
+ def freshness_ref(aVariableRef)
153
266
  walker = AssociationWalker.new
154
267
  walker.determine_freshness(aVariableRef, self)
155
268
  end
@@ -161,33 +274,124 @@ module MiniKraken
161
274
  walker.quote_term(aVariableRef, self)
162
275
  end
163
276
 
164
- # @param aName [String]
165
- # @return [Array<Association>]
166
- def [](aName)
167
- assoc_arr = associations[aName]
168
- assoc_arr = [] if assoc_arr.nil?
277
+ # Return the variable with given user-defined variable name.
278
+ # @param aName [String] User-defined variable name
279
+ def name2var(aName)
280
+ var = nil
281
+ walker = ancestor_walker
169
282
 
170
- assoc_arr.concat(parent[aName]) if parent
171
- assoc_arr
283
+ loop do
284
+ voc = walker.next
285
+ if voc
286
+ next unless voc.respond_to?(:vars) && voc.vars.include?(aName)
287
+
288
+ var = voc.vars[aName]
289
+ end
290
+
291
+ break
292
+ end
293
+
294
+ var
172
295
  end
173
296
 
174
- # Check that a variable with given name is defined in this vocabulary
175
- # of one of its ancestor.
176
- # @return [Boolean]
177
- def include?(aVarName)
178
- var_found = false
297
+ # Return the variable with given internal variable name.
298
+ # @param i_name [String] internal variable name
299
+ # @return [Variable]
300
+ def i_name2var(i_name)
301
+ var = nil
302
+ voc = nil
179
303
  walker = ancestor_walker
304
+
180
305
  loop do
181
- voc = walker.resume
306
+ voc = walker.next
182
307
  if voc
183
- next unless voc.respond_to?(:vars) && voc.vars.include?(aVarName)
184
- var_found = true
308
+ next unless voc.respond_to?(:ivars) && voc.ivars.include?(i_name)
309
+
310
+ var_name = voc.ivars[i_name].first # TODO: what if multiple vars?
311
+ var = voc.vars[var_name]
185
312
  end
186
313
 
187
314
  break
188
315
  end
189
316
 
190
- var_found
317
+ raise StandardError, 'Nil variable object' if var.nil?
318
+
319
+ var
320
+ end
321
+
322
+ # Return the internal name to corresponding to a given user-defined
323
+ # variable name.
324
+ # @param aName [String] User-defined variable name
325
+ def to_internal(aName)
326
+ var = name2var(aName)
327
+ var ? var.i_name : nil
328
+ end
329
+
330
+ # Return the internal names fused with given user-defined
331
+ # variable name.
332
+ # @param aName [String] User-defined variable name
333
+ def names_fused(aName)
334
+ var = name2var(aName)
335
+ return [] unless var.fused?
336
+
337
+ i_name = var.i_name
338
+ names = []
339
+ walker = ancestor_walker
340
+
341
+ loop do
342
+ voc = walker.next
343
+ break unless voc
344
+ next unless voc.respond_to?(:ivars)
345
+
346
+ if voc.ivars.include?(i_name)
347
+ fused = voc.ivars[i_name]
348
+ names.concat(fused.to_a)
349
+ end
350
+ end
351
+
352
+ names.uniq!
353
+ names.reject { |nm| nm == aName }
354
+ end
355
+
356
+ # Retrieve all the associations for a given variable
357
+ # @param aVariable [Variable]
358
+ # @return [Array<Association>]
359
+ def assocs4var(aVariable)
360
+ i_name = aVariable.i_name
361
+ assocs = []
362
+ walker = ancestor_walker
363
+
364
+ loop do
365
+ voc = walker.next
366
+ break unless voc
367
+ next unless voc.associations.include?(i_name)
368
+
369
+ assocs.concat(voc.associations[i_name])
370
+ end
371
+
372
+ assocs
373
+ end
374
+
375
+ # @param aName [String] User-defined variable name
376
+ # @return [Array<Association>]
377
+ def [](aName)
378
+ iname = to_internal(aName)
379
+ return [] unless iname
380
+
381
+ assoc_arr = associations[iname]
382
+ assoc_arr = [] if assoc_arr.nil?
383
+
384
+ # TODO: Optimize
385
+ assoc_arr.concat(parent[aName]) if parent
386
+ assoc_arr
387
+ end
388
+
389
+ # Check that a variable with given name is defined in this vocabulary
390
+ # or one of its ancestor.
391
+ # @param aVarName [String] A user-defined variable name.
392
+ # @return [Boolean]
393
+ def include?(aVarName)
394
+ name2var(aVarName) ? true : false
191
395
  end
192
396
 
193
397
  protected
@@ -201,6 +405,21 @@ module MiniKraken
201
405
 
202
406
  aParent
203
407
  end
408
+
409
+ # @param theAssociations [Array<Association>]
410
+ def do_add_assocs(theAssociations)
411
+ theAssociations.each do |assc|
412
+ i_name = assc.i_name
413
+ found_assocs = associations[i_name]
414
+ if found_assocs
415
+ found_assocs << assc
416
+ else
417
+ associations[i_name] = [assc]
418
+ end
419
+
420
+ assc
421
+ end
422
+ end
204
423
  end # class
205
424
  end # module
206
- end # module
425
+ end # module
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../core/environment'
2
4
  require_relative '../core/variable'
3
5
 
@@ -20,12 +22,12 @@ module MiniKraken
20
22
  end
21
23
 
22
24
  # Attempt to achieve the goal given this environment
23
- # @param aParent [Environment]
24
- # @return [Fiber<Outcome>] A Fiber object that will generate the results.
25
+ # @param aParent [Environment]
26
+ # @return [Fiber<Outcome>] A Fiber object that will generate the results.
25
27
  def attain(aParent)
26
28
  self.parent = aParent
27
29
  goal.attain(self)
28
30
  end
29
31
  end # class
30
32
  end # module
31
- end # module
33
+ end # module
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../core/any_value'
2
4
  require_relative '../core/cons_cell'
3
5
  require_relative 'fresh_env'
@@ -19,21 +21,29 @@ module MiniKraken
19
21
 
20
22
  def run
21
23
  result = nil
24
+ next_result = nil
22
25
  solver = env.goal.attain(env)
23
- # require 'debug'
26
+ # require 'debug'
24
27
  loop do
28
+ env.clear
29
+ env.clear_rankings
25
30
  outcome = solver.resume
26
31
  break if outcome.nil?
27
- env.clear
32
+
28
33
  if result # ... more than one result...
29
- else
30
34
  if outcome.successful?
31
- env.propagate(outcome)
32
- # require 'debug'
33
- result = Core::ConsCell.new(var.quote(outcome))
35
+ next_result.append(Core::ConsCell.new(var.quote(outcome)))
34
36
  else
35
- result = Core::NullList
37
+ next_result.append(Core::NullList)
36
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
37
47
  end
38
48
  end
39
49
 
@@ -41,4 +51,4 @@ module MiniKraken
41
51
  end
42
52
  end # class
43
53
  end # module
44
- end # module
54
+ end # module