speculation 0.1.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +87 -0
  4. data/.travis.yml +16 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +116 -0
  9. data/Rakefile +29 -0
  10. data/bin/bundler +17 -0
  11. data/bin/byebug +17 -0
  12. data/bin/coderay +17 -0
  13. data/bin/console +70 -0
  14. data/bin/cucumber-queue +17 -0
  15. data/bin/minitest-queue +17 -0
  16. data/bin/pry +17 -0
  17. data/bin/rake +17 -0
  18. data/bin/rspec-queue +17 -0
  19. data/bin/rubocop +17 -0
  20. data/bin/ruby-parse +17 -0
  21. data/bin/ruby-rewrite +17 -0
  22. data/bin/setup +8 -0
  23. data/bin/testunit-queue +17 -0
  24. data/bin/yard +17 -0
  25. data/bin/yardoc +17 -0
  26. data/bin/yri +17 -0
  27. data/lib/speculation/conj.rb +32 -0
  28. data/lib/speculation/error.rb +17 -0
  29. data/lib/speculation/gen.rb +106 -0
  30. data/lib/speculation/identifier.rb +47 -0
  31. data/lib/speculation/namespaced_symbols.rb +28 -0
  32. data/lib/speculation/pmap.rb +30 -0
  33. data/lib/speculation/spec_impl/and_spec.rb +39 -0
  34. data/lib/speculation/spec_impl/every_spec.rb +176 -0
  35. data/lib/speculation/spec_impl/f_spec.rb +121 -0
  36. data/lib/speculation/spec_impl/hash_spec.rb +215 -0
  37. data/lib/speculation/spec_impl/merge_spec.rb +40 -0
  38. data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
  39. data/lib/speculation/spec_impl/or_spec.rb +62 -0
  40. data/lib/speculation/spec_impl/regex_spec.rb +35 -0
  41. data/lib/speculation/spec_impl/spec.rb +47 -0
  42. data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
  43. data/lib/speculation/spec_impl.rb +36 -0
  44. data/lib/speculation/test.rb +553 -0
  45. data/lib/speculation/utils.rb +64 -0
  46. data/lib/speculation/utils_specs.rb +57 -0
  47. data/lib/speculation/version.rb +4 -0
  48. data/lib/speculation.rb +1308 -0
  49. data/speculation.gemspec +43 -0
  50. metadata +246 -0
@@ -0,0 +1,1308 @@
1
+ # frozen_string_literal: true
2
+ require "concurrent"
3
+ require "set"
4
+ require "securerandom"
5
+
6
+ require "speculation/version"
7
+ require "speculation/namespaced_symbols"
8
+ require "speculation/conj"
9
+ require "speculation/identifier"
10
+ require "speculation/utils"
11
+ require "speculation/spec_impl"
12
+ require "speculation/error"
13
+
14
+ module Speculation
15
+ using NamespacedSymbols.refine(self)
16
+ using Conj
17
+
18
+ class << self
19
+ # Enables or disables spec asserts. Defaults to false.
20
+ attr_accessor :check_asserts
21
+
22
+ # A soft limit on how many times a branching spec (or/alt/zero_or_more) can
23
+ # be recursed through during generation. After this a non-recursive branch
24
+ # will be chosen.
25
+ attr_accessor :recursion_limit
26
+
27
+ # The number of times an anonymous fn specified by fspec will be
28
+ # (generatively) tested during conform.
29
+ attr_accessor :fspec_iterations
30
+
31
+ # The number of elements validated in a collection spec'ed with 'every'.
32
+ attr_accessor :coll_check_limit
33
+
34
+ # The number of errors reported by explain in a collection spec'ed with
35
+ # 'every'
36
+ attr_accessor :coll_error_limit
37
+ end
38
+
39
+ @check_asserts = false
40
+ @recursion_limit = 4
41
+ @fspec_iterations = 21
42
+ @coll_check_limit = 101
43
+ @coll_error_limit = 20
44
+
45
+ @registry_ref = Concurrent::Atom.new({})
46
+
47
+ # Can be enabled/disabled by setting check_asserts.
48
+ # @param spec [Spec]
49
+ # @param x value to validate
50
+ # @return x if x is valid? according to spec
51
+ # @raise [Error] with explain_data plus :Speculation/failure of :assertion_failed
52
+ def self.assert(spec, x)
53
+ return x unless check_asserts
54
+ return x unless valid?(spec, x)
55
+
56
+ ed = S._explain_data(spec, [], [], [], x)
57
+ out = StringIO.new
58
+ S.explain_out(ed, out)
59
+
60
+ raise Speculation::Error.new("Spec assertion failed\n#{out.string}", :failure.ns => :assertion_failed)
61
+ end
62
+
63
+ # @param infinite [Boolean] whether +/- infinity allowed (default true)
64
+ # @param nan [Boolean] whether Flaot::NAN allowed (default true)
65
+ # @param min [Boolean] minimum value (inclusive, default none)
66
+ # @param max [Boolean] maximum value (inclusive, default none)
67
+ # @return [Spec] that validates floats
68
+ def self.float_in(min: nil, max: nil, infinite: true, nan: true)
69
+ preds = [Float]
70
+ preds << ->(x) { !x.nan? } unless nan
71
+ preds << ->(x) { !x.infinite? } unless infinite
72
+ preds << ->(x) { x <= max } if max
73
+ preds << ->(x) { x >= min } if min
74
+
75
+ min ||= Float::MIN
76
+ max ||= Float::MAX
77
+
78
+ gens = [[20, ->(_) { rand(min.to_f..max.to_f) }]]
79
+ gens << [1, ->(_) { Float::INFINITY }] if infinite
80
+ gens << [1, ->(_) { Float::NAN }] if nan
81
+
82
+ spec(S.and(*preds), :gen => ->(rantly) { rantly.freq(*gens) })
83
+ end
84
+
85
+ # @param range [Range<Integer>]
86
+ # @return Spec that validates ints in the given range
87
+ def self.int_in(range)
88
+ spec(S.and(Integer, ->(x) { range.include?(x) }),
89
+ :gen => ->(_) { rand(range) })
90
+ end
91
+
92
+ # @param time_range [Range<Time>]
93
+ # @return Spec that validates times in the given range
94
+ def self.time_in(time_range)
95
+ spec(S.and(Time, ->(x) { time_range.cover?(x) }),
96
+ :gen => ->(_) { rand(time_range) })
97
+ end
98
+
99
+ # @param date_range [Range<Date>]
100
+ # @return Spec that validates dates in the given range
101
+ def self.date_in(date_range)
102
+ spec(S.and(Date, ->(x) { date_range.cover?(x) }),
103
+ :gen => ->(_) { rand(date_range) })
104
+ end
105
+
106
+ # @param x [Spec, Object]
107
+ # @return [Spec, false] x if x is a spec, else false
108
+ def self.spec?(x)
109
+ x if x.is_a?(SpecImpl)
110
+ end
111
+
112
+ # @param x [Hash, Object]
113
+ # @return [Hash, false] x if x is a (Speculation) regex op, else logical false
114
+ def self.regex?(x)
115
+ Utils.hash?(x) && x[:op.ns] && x
116
+ end
117
+
118
+ # @param value return value of a `conform` call
119
+ # @return [Boolean] true if value is the result of an unsuccessful conform
120
+ def self.invalid?(value)
121
+ value.equal?(:invalid.ns)
122
+ end
123
+
124
+ # @param spec [Spec]
125
+ # @param value value to conform
126
+ # @return [Symbol, Object] :Speculation/invalid if value does not match spec, else the (possibly destructured) value
127
+ def self.conform(spec, value)
128
+ spec = Identifier(spec)
129
+ specize(spec).conform(value)
130
+ end
131
+
132
+ # Takes a spec and a one-arg generator block and returns a version of the spec that uses that generator
133
+ # @param spec [Spec]
134
+ # @yield [Rantly] generator block
135
+ # @return [Spec]
136
+ def self.with_gen(spec, &gen)
137
+ if regex?(spec)
138
+ spec.merge(:gfn.ns => gen)
139
+ else
140
+ specize(spec).tap { |s| s.gen = gen }
141
+ end
142
+ end
143
+
144
+ # @private
145
+ def self._explain_data(spec, path, via, inn, value)
146
+ probs = specize(spec).explain(path, via, inn, value)
147
+
148
+ if probs && probs.any?
149
+ { :problems.ns => probs }
150
+ end
151
+ end
152
+
153
+ # Given a spec and a value x which ought to conform, returns nil if x
154
+ # conforms, else a hash with at least the key :"Speculation/problems" whose
155
+ # value is a collection of problem-hashes, where problem-hash has at least
156
+ # :path :pred and :val keys describing the predicate and the value that failed
157
+ # at that path.
158
+ # @param spec [Spec]
159
+ # @param x value which ought to conform
160
+ # @return [nil, Hash] nil if x conforms, else a hash with at least the key
161
+ # :Speculation/problems whose value is a collection of problem-hashes,
162
+ # where problem-hash has at least :path :pred and :val keys describing the
163
+ # predicate and the value that failed at that path.
164
+ def self.explain_data(spec, x)
165
+ spec = Identifier(spec)
166
+ name = spec_name(spec)
167
+ _explain_data(spec, [], Array(name), [], x)
168
+ end
169
+
170
+ # @param ed [Hash] explain data (per 'explain_data')
171
+ # @param out [IO] destination to write explain human readable message to (default STDOUT)
172
+ def self.explain_out(ed, out = STDOUT)
173
+ return out.puts("Success!") unless ed
174
+
175
+ ed.fetch(:problems.ns).each do |prob|
176
+ path, pred, val, reason, via, inn = prob.values_at(:path, :pred, :val, :reason, :via, :in)
177
+
178
+ out.print("In: ", inn.to_a.inspect, " ") unless inn.empty?
179
+ out.print("val: ", val.inspect, " fails")
180
+ out.print(" spec: ", via.last.inspect) unless via.empty?
181
+ out.print(" at: ", path.to_a.inspect) unless path.empty?
182
+ out.print(" predicate: ", pred.inspect)
183
+ out.print(", ", reason.inspect) if reason
184
+
185
+ prob.each do |k, v|
186
+ unless [:path, :pred, :val, :reason, :via, :in].include?(k)
187
+ out.print("\n\t ", k.inspect, v.inspect)
188
+ end
189
+ end
190
+
191
+ out.puts
192
+ end
193
+
194
+ ed.each do |k, v|
195
+ out.puts(k, v) unless k == :problems.ns
196
+ end
197
+
198
+ nil
199
+ end
200
+
201
+ # Given a spec and a value that fails to conform, prints an explaination to STDOUT
202
+ # @param spec [Spec]
203
+ # @param x
204
+ def self.explain(spec, x)
205
+ explain_out(explain_data(spec, x))
206
+ end
207
+
208
+ # @param spec [Spec]
209
+ # @param x a value that fails to conform
210
+ # @return [String] a human readable explaination
211
+ def self.explain_str(spec, x)
212
+ out = StringIO.new
213
+ explain_out(explain_data(spec, x), out)
214
+ out.string
215
+ end
216
+
217
+ # @private
218
+ def self.gensub(spec, overrides, path, rmap)
219
+ overrides ||= {}
220
+
221
+ spec = specize(spec)
222
+ gfn = overrides[spec_name(spec) || spec] || overrides[path]
223
+ g = gfn || spec.gen(overrides, path, rmap)
224
+
225
+ if g
226
+ Gen.such_that(->(x) { valid?(spec, x) }, g)
227
+ else
228
+ raise Speculation::Error.new("unable to construct gen at: #{path.inspect} for: #{spec.inspect}",
229
+ :failure.ns => :no_gen, :path.ns => path)
230
+ end
231
+ end
232
+
233
+ # Given a spec, returns the generator for it, or raises if none can be
234
+ # constructed.
235
+ #
236
+ # Optionally an overrides hash can be provided which should map
237
+ # spec names or paths (array of symbols) to no-arg generator Procs.
238
+ # These will be used instead of the generators at those names/paths. Note that
239
+ # parent generator (in the spec or overrides map) will supersede those of any
240
+ # subtrees. A generator for a regex op must always return a sequential
241
+ # collection (i.e. a generator for Speculation.zero_or_more should return
242
+ # either an empty array or an array with one item in it)
243
+ #
244
+ # @param spec [Spec]
245
+ # @param overrides <Hash>
246
+ # @return [Proc]
247
+ def self.gen(spec, overrides = nil)
248
+ spec = Identifier(spec)
249
+ gensub(spec, overrides, [], :recursion_limit.ns => recursion_limit)
250
+ end
251
+
252
+ # rubocop:disable Style/MethodName
253
+ # @private
254
+ def self.Identifier(x)
255
+ case x
256
+ when Method then Identifier.new(x.receiver, x.name, false)
257
+ when UnboundMethod then Identifier.new(x.owner, x.name, true)
258
+ else x
259
+ end
260
+ end
261
+ # rubocop:enable Style/MethodName
262
+
263
+ # Given a namespace-qualified symbol key, and a spec, spec name, predicate or
264
+ # regex-op makes an entry in the registry mapping key to the spec
265
+ # @param key [Symbol] namespace-qualified symbol
266
+ # @param spec [Spec, Symbol, Proc, Hash] a spec, spec name, predicate or regex-op
267
+ # @return [Symbol, Identifier]
268
+ def self.def(key, spec)
269
+ key = Identifier(key)
270
+
271
+ unless Utils.ident?(key) && key.namespace
272
+ raise ArgumentError,
273
+ "key must be a namespaced Symbol, e.g. #{:my_spec.ns}, given #{key}, or a Speculation::Identifier"
274
+ end
275
+
276
+ spec = if spec?(spec) || regex?(spec) || registry[spec]
277
+ spec
278
+ else
279
+ spec_impl(spec, false)
280
+ end
281
+
282
+ @registry_ref.swap do |reg|
283
+ reg.merge(key => with_name(spec, key))
284
+ end
285
+
286
+ key
287
+ end
288
+
289
+ # @return [Hash] the registry hash
290
+ # @see get_spec
291
+ def self.registry
292
+ @registry_ref.value
293
+ end
294
+
295
+ # @param key [Symbol, Method]
296
+ # @return [Spec, nil] spec registered for key, or nil
297
+ def self.get_spec(key)
298
+ registry[Identifier(key)]
299
+ end
300
+
301
+ # NOTE: it is not generally necessary to wrap predicates in spec when using
302
+ # `S.def` etc., only to attach a unique generator.
303
+ #
304
+ # Optionally takes :gen generator function, which must be a proc of one arg
305
+ # (Rantly instance) that generates a valid value.
306
+ #
307
+ # @param pred [Proc, Method, Set, Class, Regexp, Hash] Takes a single predicate. A
308
+ # predicate can be one of:
309
+ #
310
+ # - Proc, e.g. `-> (x) { x.even? }`, will be called with the given value
311
+ # - Method, e.g. `Foo.method(:bar?)`, will be called with the given value
312
+ # - Set, e.g. `Set[1, 2]`, will be tested whether it includes the given value
313
+ # - Class/Module, e.g. `String`, will be tested for case equality (is_a?)
314
+ # with the given value
315
+ # - Regexp, e.g. `/foo/`, will be tested using `===` with given value
316
+ #
317
+ # Can also be passed the result of one of the regex ops - cat, alt,
318
+ # zero_or_more, one_or_more, zero_or_one, in which case it will return a
319
+ # regex-conforming spec, useful when nesting an independent regex.
320
+ #
321
+ # @param gen [Proc] generator function, which must be a proc of one
322
+ # arg (Rantly instance) that generates a valid value.
323
+ # @return [Spec]
324
+ def self.spec(pred, gen: nil)
325
+ if pred
326
+ spec_impl(pred, false).tap do |spec|
327
+ spec.gen = gen if gen
328
+ end
329
+ end
330
+ end
331
+
332
+ # Creates and returns a hash validating spec. :req and :opt are both arrays of
333
+ # namespaced-qualified keywords (e.g. ":MyApp/foo"). The validator will ensure
334
+ # the :req keys are present. The :opt keys serve as documentation and may be
335
+ # used by the generator.
336
+ #
337
+ # The :req key array supports 'and_keys' and 'or_keys' for key groups:
338
+ #
339
+ # S.keys(req: [:x.ns, :y.ns, S.or_keys(:secret.ns, S.and_keys(:user.ns, :pwd.ns))],
340
+ # opt: [:z.ns])
341
+ #
342
+ # There are also _un versions of :req and :opt. These allow you to connect
343
+ # unqualified keys to specs. In each case, fully qualfied keywords are passed,
344
+ # which name the specs, but unqualified keys (with the same name component)
345
+ # are expected and checked at conform-time, and generated during gen:
346
+ #
347
+ # S.keys(req_un: [:"MyApp/x", :"MyApp/y"])
348
+ #
349
+ # The above says keys :x and :y are required, and will be validated and
350
+ # generated by specs (if they exist) named :"MyApp/x" :"MyApp/y" respectively.
351
+ #
352
+ # In addition, the values of *all* namespace-qualified keys will be validated
353
+ # (and possibly destructured) by any registered specs. Note: there is
354
+ # no support for inline value specification, by design.
355
+ #
356
+ # @param req [Array<Symbol>]
357
+ # @param opt [Array<Symbol>]
358
+ # @param req_un [Array<Symbol>]
359
+ # @param opt_un [Array<Symbol>]
360
+ # @param gen [Proc] generator function, which must be a proc of one arg
361
+ # (Rantly instance) that generates a valid value
362
+ def self.keys(req: [], opt: [], req_un: [], opt_un: [], gen: nil)
363
+ HashSpec.new(req, opt, req_un, opt_un).tap do |spec|
364
+ spec.gen = gen
365
+ end
366
+ end
367
+
368
+ # @see keys
369
+ def self.or_keys(*ks)
370
+ [:or.ns, *ks]
371
+ end
372
+
373
+ # @see keys
374
+ def self.and_keys(*ks)
375
+ [:and.ns, *ks]
376
+ end
377
+
378
+ # @param key_preds [Hash] Takes key+pred hash
379
+ # @return [Spec] a destructuring spec that returns a two element array containing the key of the first
380
+ # matching pred and the corresponding value. Thus the 'key' and 'val' functions can be used to
381
+ # refer generically to the components of the tagged return.
382
+ # @example
383
+ # S.or(even: -> (n) { n.even? }, small: -> (n) { n < 42 })
384
+ def self.or(key_preds)
385
+ OrSpec.new(key_preds)
386
+ end
387
+
388
+ # @param preds [Array] predicate/specs
389
+ # @return [Spec] a spec that returns the conformed value. Successive
390
+ # conformed values propagate through rest of predicates.
391
+ # @example
392
+ # S.and(Numeric, -> (n) { n < 42 })
393
+ def self.and(*preds)
394
+ AndSpec.new(preds)
395
+ end
396
+
397
+ # @param preds [Array] hash-validating specs (e.g. 'keys' specs)
398
+ # @return [Spec] a spec that returns a conformed hash satisfying all of the specs.
399
+ # @note Unlike 'and', merge can generate maps satisfying the union of the predicates.
400
+ def self.merge(*preds)
401
+ MergeSpec.new(preds)
402
+ end
403
+
404
+ # @note that 'every' does not do exhaustive checking, rather it samples
405
+ # `coll_check_limit` elements. Nor (as a result) does it do any conforming of
406
+ # elements. 'explain' will report at most coll_error_limit problems. Thus
407
+ # 'every' should be suitable for potentially large collections.
408
+ # @param pred predicate to validate collections with
409
+ # @param opts [Hash] Takes several kwargs options that further constrain the collection:
410
+ # @option opts :kind (nil) a pred/spec that the collection type must satisfy, e.g. `Array`
411
+ # Note that if :kind is specified and :into is not, this pred must generate in order for every
412
+ # to generate.
413
+ # @option opts :count [Integer] (nil) specifies coll has exactly this count
414
+ # @option opts :min_count [Integer] (nil) coll has count >= min_count
415
+ # @option opts :max_count [Integer] (nil) coll has count <= max_count
416
+ # @option opts :distinct [Boolean] (nil) all the elements are distinct
417
+ # @option opts :gen_max [Integer] (20) the maximum coll size to generate
418
+ # @option opts :into [Array, Hash, Set] (Array) one of [], {}, Set[], the
419
+ # default collection to generate into (default: empty coll as generated by
420
+ # :kind pred if supplied, else [])
421
+ # @option opts :gen [Proc] generator proc, which must be a proc of one arg
422
+ # (Rantly instance) that generates a valid value.
423
+ # @see coll_of
424
+ # @see every_kv
425
+ # @return [Spec] spec that validates collection elements against pred
426
+ def self.every(pred, opts = {})
427
+ gen = opts.delete(:gen)
428
+
429
+ EverySpec.new(pred, opts).tap do |spec|
430
+ spec.gen = gen
431
+ end
432
+ end
433
+
434
+ # Like 'every' but takes separate key and val preds and works on associative collections.
435
+ #
436
+ # Same options as 'every', :into defaults to {}
437
+ #
438
+ # @see every
439
+ # @see hash_of
440
+ # @param kpred key pred
441
+ # @param vpred val pred
442
+ # @param options [Hash]
443
+ # @return [Spec] spec that validates associative collections
444
+ def self.every_kv(kpred, vpred, options)
445
+ every(tuple(kpred, vpred), :kfn.ns => ->(_i, v) { v.first },
446
+ :into => {},
447
+ **options)
448
+ end
449
+
450
+ # Returns a spec for a collection of items satisfying pred. Unlike 'every', coll_of will
451
+ # exhaustively conform every value.
452
+ #
453
+ # Same options as 'every'. conform will produce a collection corresponding to :into if supplied,
454
+ # else will match the input collection, avoiding rebuilding when possible.
455
+ #
456
+ # @see every
457
+ # @see hash_of
458
+ # @param pred
459
+ # @param opts [Hash]
460
+ # @return [Spec]
461
+ def self.coll_of(pred, opts = {})
462
+ every(pred, :conform_all.ns => true, **opts)
463
+ end
464
+
465
+ # Returns a spec for a hash whose keys satisfy kpred and vals satisfy vpred.
466
+ # Unlike 'every_kv', hash_of will exhaustively conform every value.
467
+ #
468
+ # Same options as 'every', :kind defaults to `Speculation::Utils.hash?`, with
469
+ # the addition of:
470
+ #
471
+ # :conform_keys - conform keys as well as values (default false)
472
+ #
473
+ # @see every_kv
474
+ # @param kpred key pred
475
+ # @param vpred val pred
476
+ # @param options [Hash]
477
+ # @return [Spec]
478
+ def self.hash_of(kpred, vpred, options = {})
479
+ every_kv(kpred, vpred, :kind => Utils.method(:hash?).to_proc,
480
+ :conform_all.ns => true,
481
+ **options)
482
+ end
483
+
484
+ # @param pred
485
+ # @return [Hash] regex op that matches zero or more values matching pred. Produces
486
+ # an array of matches iff there is at least one match
487
+ def self.zero_or_more(pred)
488
+ rep(pred, pred, [], false)
489
+ end
490
+
491
+ # @param pred
492
+ # @return [Hash] regex op that matches one or more values matching pred. Produces
493
+ # an array of matches
494
+ def self.one_or_more(pred)
495
+ pcat(:predicates => [pred, rep(pred, pred, [], true)], :return_value => [])
496
+ end
497
+
498
+ # @param pred
499
+ # @return [Hash] regex op that matches zero or one value matching pred. Produces a
500
+ # single value (not a collection) if matched.
501
+ def self.zero_or_one(pred)
502
+ _alt([pred, accept(:nil.ns)], nil)
503
+ end
504
+
505
+ # @param kv_specs [Hash] key+pred pairs
506
+ # @example
507
+ # S.alt(even: :even?.to_proc, small: -> (n) { n < 42 })
508
+ # @return [Hash] regex op that returns a two item array containing the key of the
509
+ # first matching pred and the corresponding value. Thus can be destructured
510
+ # to refer generically to the components of the return.
511
+ def self.alt(kv_specs)
512
+ _alt(kv_specs.values, kv_specs.keys).merge(:id => SecureRandom.uuid)
513
+ end
514
+
515
+ # @example
516
+ # S.cat(e: :even?.to_proc, o: :odd?.to_proc)
517
+ # @param named_specs [Hash] key+pred hash
518
+ # @return [Hash] regex op that matches (all) values in sequence, returning a map
519
+ # containing the keys of each pred and the corresponding value.
520
+ def self.cat(named_specs)
521
+ keys = named_specs.keys
522
+ predicates = named_specs.values
523
+
524
+ pcat(:keys => keys, :predicates => predicates, :return_value => {})
525
+ end
526
+
527
+ # @param re [Hash] regex op
528
+ # @param preds [Array] predicates
529
+ # @return [Hash] regex-op that consumes input as per re but subjects the
530
+ # resulting value to the conjunction of the predicates, and any conforming
531
+ # they might perform.
532
+ def self.constrained(re, *preds)
533
+ { :op.ns => :amp.ns, :p1 => re, :predicates => preds }
534
+ end
535
+
536
+ # @param f predicate function with the semantics of conform i.e. it should
537
+ # return either a (possibly converted) value or :"Speculation/invalid"
538
+ # @return [Spec] a spec that uses pred as a predicate/conformer.
539
+ def self.conformer(f)
540
+ spec_impl(f, true)
541
+ end
542
+
543
+ # Takes :args :ret and (optional) :block and :fn kwargs whose values are preds and returns a spec
544
+ # whose conform/explain take a method/proc and validates it using generative testing. The
545
+ # conformed value is always the method itself.
546
+ #
547
+ # fspecs can generate procs that validate the arguments and fabricate a return value compliant
548
+ # with the :ret spec, ignoring the :fn spec if present.
549
+ #
550
+ # @param args predicate
551
+ # @param ret predicate
552
+ # @param fn predicate
553
+ # @param block predicate
554
+ # @param gen [Proc] generator proc, which must be a proc of one arg (Rantly
555
+ # instance) that generates a valid value.
556
+ # @return [Spec]
557
+ # @see fdef See 'fdef' for a single operation that creates an fspec and registers it, as well as a full description of :args, :block, :ret and :fn
558
+ def self.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
559
+ FSpec.new(:argspec => spec(args), :retspec => spec(ret), :fnspec => spec(fn), :blockspec => spec(block)).tap do |spec|
560
+ spec.gen = gen
561
+ end
562
+ end
563
+
564
+ # @param preds [Array] one or more preds
565
+ # @return [Spec] a spec for a tuple, an array where each element conforms to
566
+ # the corresponding pred. Each element will be referred to in paths using its
567
+ # ordinal.
568
+ def self.tuple(*preds)
569
+ TupleSpec.new(preds)
570
+ end
571
+
572
+ # Once registered, specs are checked by instrument and tested by the runner Speculation::Test.check
573
+ #
574
+ # @example to register method specs for the Hash[] method:
575
+ # S.fdef(Hash.method(:[]),
576
+ # args: S.alt(
577
+ # hash: Hash,
578
+ # array_of_pairs: S.coll_of(S.tuple(:any.ns(S), :any.ns(S)), kind: Array),
579
+ # kvs: S.constrained(S.one_or_more(:any.ns(S)), -> (kvs) { kvs.count.even? })
580
+ # ),
581
+ # ret: Hash
582
+ # )
583
+ #
584
+ # @param method [Method]
585
+ # @param spec [Hash]
586
+ # @option spec :args [Hash] regex spec for the method arguments as a list
587
+ # @option spec :block an fspec for the method's block
588
+ # @option spec :ret a spec for the method's return value
589
+ # @option spec :fn a spec of the relationship between args and ret - the value passed is
590
+ # { args: conformed_args, block: given_block, ret: conformed_ret } and is expected to contain
591
+ # predicates that relate those values
592
+ # @return [Identifier] the Speculation::Identifier object representing the method which is used as the spec's
593
+ # key in the spec registry.
594
+ # @note Note that :fn specs require the presence of :args and :ret specs to conform values, and so :fn
595
+ # specs will be ignored if :args or :ret are missing.
596
+ def self.fdef(method, spec)
597
+ ident = Identifier(method)
598
+ self.def(ident, fspec(spec))
599
+ end
600
+
601
+ # @param spec
602
+ # @param x
603
+ # @return [Boolean] true when x is valid for spec.
604
+ def self.valid?(spec, x)
605
+ spec = Identifier(spec)
606
+ spec = specize(spec)
607
+
608
+ !invalid?(spec.conform(x))
609
+ end
610
+
611
+ # @param pred
612
+ # @return [Spec] a spec that accepts nil and values satisfying pred
613
+ def self.nilable(pred)
614
+ NilableSpec.new(pred)
615
+ end
616
+
617
+ # Generates a number (default 10) of values compatible with spec and maps
618
+ # conform over them, returning a sequence of [val conformed-val] tuples.
619
+ # @param spec
620
+ # @param n [Integer]
621
+ # @param overrides [Hash] a generator overrides hash as per `gen`
622
+ # @return [Array] an array of [val, conformed_val] tuples
623
+ # @see gen for generator overrides
624
+ def self.exercise(spec, n: 10, overrides: {})
625
+ Gen.sample(gen(spec, overrides), n).map { |value|
626
+ [value, conform(spec, value)]
627
+ }
628
+ end
629
+
630
+ # Exercises the method by applying it to n (default 10) generated samples of
631
+ # its args spec. When fspec is supplied its arg spec is used, and method can
632
+ # be a proc.
633
+ # @param method [Method]
634
+ # @param n [Integer]
635
+ # @param fspec [Spec]
636
+ # @return [Array] an arrray of tuples of [args, ret].
637
+ def self.exercise_fn(method, n: 10, fspec: nil)
638
+ fspec ||= get_spec(method)
639
+ raise ArgumentError, "No fspec found for #{method}" unless fspec
640
+
641
+ Gen.sample(gen(fspec.argspec), n).map { |args| [args, method.call(*args)] }
642
+ end
643
+
644
+ ### impl ###
645
+
646
+ # @private
647
+ def self.recur_limit?(rmap, id, path, k)
648
+ rmap[id] > rmap[:recursion_limit.ns] &&
649
+ path.include?(k)
650
+ end
651
+
652
+ # @private
653
+ def self.inck(h, k)
654
+ h.merge(k => h.fetch(k, 0).next)
655
+ end
656
+
657
+ # @private
658
+ def self.dt(pred, x)
659
+ return x unless pred
660
+
661
+ spec = the_spec(pred)
662
+
663
+ if spec
664
+ conform(spec, x)
665
+ elsif pred.is_a?(Module) || pred.is_a?(::Regexp)
666
+ pred === x ? x : :invalid.ns
667
+ elsif pred.is_a?(Set)
668
+ pred.include?(x) ? x : :invalid.ns
669
+ elsif pred.respond_to?(:call)
670
+ pred.call(x) ? x : :invalid.ns
671
+ else
672
+ raise "#{pred} is not a class, proc, set or regexp"
673
+ end
674
+ end
675
+
676
+ # internal helper function that returns true when x is valid for spec.
677
+ # @private
678
+ def self.pvalid?(pred, x)
679
+ !invalid?(dt(pred, x))
680
+ end
681
+
682
+ # @private
683
+ def self.explain1(pred, path, via, inn, value)
684
+ spec = maybe_spec(pred)
685
+
686
+ if spec?(spec)
687
+ name = spec_name(spec)
688
+ via = via.conj(name) if name
689
+
690
+ spec.explain(path, via, inn, value)
691
+ else
692
+ [{ :path => path, :val => value, :via => via, :in => inn, :pred => pred }]
693
+ end
694
+ end
695
+
696
+ # @private
697
+ def self.spec_impl(pred, should_conform)
698
+ if spec?(pred)
699
+ pred
700
+ elsif regex?(pred)
701
+ RegexSpec.new(pred)
702
+ elsif Utils.ident?(pred)
703
+ the_spec(pred)
704
+ else
705
+ Spec.new(pred, should_conform)
706
+ end
707
+ end
708
+
709
+ # @private
710
+ def self.explain_pred_list(preds, path, via, inn, value)
711
+ return_value = value
712
+
713
+ preds.each do |pred|
714
+ nret = dt(pred, return_value)
715
+
716
+ if invalid?(nret)
717
+ return explain1(pred, path, via, inn, return_value)
718
+ else
719
+ return_value = nret
720
+ end
721
+ end
722
+
723
+ nil
724
+ end
725
+
726
+ ### regex
727
+
728
+ # @private
729
+ def self.re_gen(p, overrides, path, rmap)
730
+ origp = p
731
+ p = reg_resolve!(p)
732
+
733
+ id, op, ps, ks, p1, p2, ret, id, gen = p.values_at(
734
+ :id, :op.ns, :predicates, :keys, :p1, :p2, :return_value, :id, :gen.ns
735
+ ) if regex?(p)
736
+
737
+ id = p.id if spec?(p)
738
+ ks ||= []
739
+
740
+ rmap = inck(rmap, id) if id
741
+
742
+ ggens = ->(preds, keys) do
743
+ preds.zip(keys).map do |pred, k|
744
+ unless rmap && id && k && recur_limit?(rmap, id, path, k)
745
+ if id
746
+ Gen.delay { Speculation.re_gen(pred, overrides, k ? path.conj(k) : path, rmap) }
747
+ else
748
+ re_gen(pred, overrides, k ? path.conj(k) : path, rmap)
749
+ end
750
+ end
751
+ end
752
+ end
753
+
754
+ ogen = overrides[spec_name(origp)] ||
755
+ overrides[spec_name(p)] ||
756
+ overrides[path]
757
+
758
+ if ogen
759
+ if [:accept, nil].include?(op)
760
+ return ->(rantly) { [*ogen.call(rantly)] }
761
+ else
762
+ return ->(rantly) { ogen.call(rantly) }
763
+ end
764
+ end
765
+
766
+ return gen if gen
767
+
768
+ if p
769
+ case op
770
+ when :accept.ns
771
+ if ret == :nil.ns
772
+ ->(_rantly) { [] }
773
+ else
774
+ ->(_rantly) { [ret] }
775
+ end
776
+ when nil
777
+ g = gensub(p, overrides, path, rmap)
778
+
779
+ ->(rantly) { [g.call(rantly)] }
780
+ when :amp.ns
781
+ re_gen(p1, overrides, path, rmap)
782
+ when :pcat.ns
783
+ gens = ggens.call(ps, ks)
784
+
785
+ if gens.all?
786
+ ->(rantly) do
787
+ gens.flat_map { |gg| gg.call(rantly) }
788
+ end
789
+ end
790
+ when :alt.ns
791
+ gens = ggens.call(ps, ks).compact
792
+
793
+ ->(rantly) { rantly.branch(*gens) } unless gens.empty?
794
+ when :rep.ns
795
+ if recur_limit?(rmap, id, [id], id)
796
+ ->(_rantly) { [] }
797
+ else
798
+ g = re_gen(p2, overrides, path, rmap)
799
+
800
+ if g
801
+ ->(rantly) do
802
+ rantly.range(0, 20).times.flat_map { g.call(rantly) }
803
+ end
804
+ end
805
+ end
806
+ end
807
+ end
808
+ end
809
+
810
+ # @private
811
+ def self.re_conform(regex, data)
812
+ x, *xs = data
813
+
814
+ if data.empty?
815
+ return :invalid.ns unless accept_nil?(regex)
816
+
817
+ return_value = preturn(regex)
818
+
819
+ if return_value == :nil.ns
820
+ nil
821
+ else
822
+ return_value
823
+ end
824
+ else
825
+ dp = deriv(regex, x)
826
+
827
+ if dp
828
+ re_conform(dp, xs)
829
+ else
830
+ :invalid.ns
831
+ end
832
+ end
833
+ end
834
+
835
+ # @private
836
+ def self.re_explain(path, via, inn, regex, input)
837
+ p = regex
838
+
839
+ input.each_with_index do |value, index|
840
+ dp = deriv(p, value)
841
+
842
+ if dp
843
+ p = dp
844
+ next
845
+ end
846
+
847
+ if accept?(p)
848
+ if p[:op.ns] == :pcat.ns
849
+ return op_explain(p, path, via, inn.conj(index), input[index..-1])
850
+ else
851
+ return [{ :path => path,
852
+ :reason => "Extra input",
853
+ :val => input,
854
+ :via => via,
855
+ :in => inn.conj(index) }]
856
+ end
857
+ else
858
+ return op_explain(p, path, via, inn.conj(index), input[index..-1]) ||
859
+ [{ :path => path,
860
+ :reason => "Extra input",
861
+ :val => input,
862
+ :via => via,
863
+ :in => inn.conj(index) }]
864
+ end
865
+ end
866
+
867
+ if accept_nil?(p)
868
+ nil # success
869
+ else
870
+ op_explain(p, path, via, inn, nil)
871
+ end
872
+ end
873
+
874
+ class << self
875
+ private
876
+
877
+ # returns the spec/regex at end of alias chain starting with k, throws if not found, k if k not ident
878
+ def reg_resolve!(key)
879
+ return key unless Utils.ident?(key)
880
+ spec = reg_resolve(key)
881
+
882
+ if spec
883
+ spec
884
+ else
885
+ raise "Unable to resolve spec: #{key}"
886
+ end
887
+ end
888
+
889
+ def deep_resolve(reg, spec)
890
+ spec = reg[spec] while Utils.ident?(spec)
891
+ spec
892
+ end
893
+
894
+ # returns the spec/regex at end of alias chain starting with k, nil if not found, k if k not ident
895
+ def reg_resolve(key)
896
+ return key unless Utils.ident?(key)
897
+
898
+ spec = @registry_ref.value[key]
899
+
900
+ if Utils.ident?(spec)
901
+ deep_resolve(registry, spec)
902
+ else
903
+ spec
904
+ end
905
+ end
906
+
907
+ def with_name(spec, name)
908
+ if Utils.ident?(spec)
909
+ spec
910
+ elsif regex?(spec)
911
+ spec.merge(:name.ns => name)
912
+ else
913
+ spec.tap { |s| s.name = name }
914
+ end
915
+ end
916
+
917
+ def spec_name(spec)
918
+ if Utils.ident?(spec)
919
+ spec
920
+ elsif regex?(spec)
921
+ spec[:name.ns]
922
+ elsif spec.respond_to?(:name)
923
+ spec.name
924
+ end
925
+ end
926
+
927
+ # spec_or_key must be a spec, regex or ident, else returns nil. Raises if
928
+ # unresolvable ident (Speculation::Utils.ident?)
929
+ def the_spec(spec_or_key)
930
+ spec = maybe_spec(spec_or_key)
931
+ return spec if spec
932
+
933
+ if Utils.ident?(spec_or_key)
934
+ raise "Unable to resolve spec: #{spec_or_key}"
935
+ end
936
+ end
937
+
938
+ # spec_or_key must be a spec, regex or resolvable ident, else returns nil
939
+ def maybe_spec(spec_or_key)
940
+ spec = (Utils.ident?(spec_or_key) && reg_resolve(spec_or_key)) ||
941
+ spec?(spec_or_key) ||
942
+ regex?(spec_or_key) ||
943
+ nil
944
+
945
+ if regex?(spec)
946
+ with_name(RegexSpec.new(spec), spec_name(spec))
947
+ else
948
+ spec
949
+ end
950
+ end
951
+
952
+ def and_preds(x, preds)
953
+ pred, *preds = preds
954
+
955
+ x = dt(pred, x)
956
+
957
+ if invalid?(x)
958
+ :invalid.ns
959
+ elsif preds.empty?
960
+ x
961
+ else
962
+ and_preds(x, preds)
963
+ end
964
+ end
965
+
966
+ def specize(spec)
967
+ if spec?(spec)
968
+ spec
969
+ else
970
+ case spec
971
+ when Symbol, Identifier
972
+ specize(reg_resolve!(spec))
973
+ else
974
+ spec_impl(spec, false)
975
+ end
976
+ end
977
+ end
978
+
979
+ ### regex ###
980
+
981
+ def accept(x)
982
+ { :op.ns => :accept.ns, :return_value => x }
983
+ end
984
+
985
+ def accept?(hash)
986
+ if hash.is_a?(Hash)
987
+ hash[:op.ns] == :accept.ns
988
+ end
989
+ end
990
+
991
+ def pcat(regex)
992
+ predicate, *rest_predicates = regex[:predicates]
993
+
994
+ keys = regex[:keys]
995
+ key, *rest_keys = keys
996
+
997
+ return unless regex[:predicates].all?
998
+
999
+ unless accept?(predicate)
1000
+ return { :op.ns => :pcat.ns,
1001
+ :predicates => regex[:predicates],
1002
+ :keys => keys,
1003
+ :return_value => regex[:return_value] }
1004
+ end
1005
+
1006
+ val = keys ? { key => predicate[:return_value] } : predicate[:return_value]
1007
+ return_value = regex[:return_value].conj(val)
1008
+
1009
+ if rest_predicates
1010
+ pcat(:predicates => rest_predicates,
1011
+ :keys => rest_keys,
1012
+ :return_value => return_value)
1013
+ else
1014
+ accept(return_value)
1015
+ end
1016
+ end
1017
+
1018
+ def rep(p1, p2, return_value, splice)
1019
+ return unless p1
1020
+
1021
+ regex = { :op.ns => :rep.ns, :p2 => p2, :splice => splice, :id => SecureRandom.uuid }
1022
+
1023
+ if accept?(p1)
1024
+ regex.merge(:p1 => p2, :return_value => return_value.conj(p1[:return_value]))
1025
+ else
1026
+ regex.merge(:p1 => p1, :return_value => return_value)
1027
+ end
1028
+ end
1029
+
1030
+ def filter_alt(ps, ks, &block)
1031
+ if ks
1032
+ pks = ps.zip(ks).select { |xs| yield(xs.first) }
1033
+ [pks.map(&:first), pks.map(&:last)]
1034
+ else
1035
+ [ps.select(&block), ks]
1036
+ end
1037
+ end
1038
+
1039
+ def _alt(predicates, keys)
1040
+ predicates, keys = filter_alt(predicates, keys, &:itself)
1041
+ return unless predicates
1042
+
1043
+ predicate, *rest_predicates = predicates
1044
+ key, *_rest_keys = keys
1045
+
1046
+ return_value = { :op.ns => :alt.ns, :predicates => predicates, :keys => keys }
1047
+ return return_value unless rest_predicates.empty?
1048
+
1049
+ return predicate unless key
1050
+ return return_value unless accept?(predicate)
1051
+
1052
+ accept([key, predicate[:return_value]])
1053
+ end
1054
+
1055
+ def alt2(p1, p2)
1056
+ if p1 && p2
1057
+ _alt([p1, p2], nil)
1058
+ else
1059
+ p1 || p2
1060
+ end
1061
+ end
1062
+
1063
+ def no_ret?(p1, pret)
1064
+ return true if pret == :nil.ns
1065
+
1066
+ regex = reg_resolve!(p1)
1067
+ op = regex[:op.ns]
1068
+
1069
+ [:rep.ns, :pcat.ns].include?(op) && pret.empty? || nil
1070
+ end
1071
+
1072
+ def accept_nil?(regex)
1073
+ regex = reg_resolve!(regex)
1074
+ return unless regex?(regex)
1075
+
1076
+ case regex[:op.ns]
1077
+ when :accept.ns then true
1078
+ when :pcat.ns then regex[:predicates].all?(&method(:accept_nil?))
1079
+ when :alt.ns then regex[:predicates].any?(&method(:accept_nil?))
1080
+ when :rep.ns then (regex[:p1] == regex[:p2]) || accept_nil?(regex[:p1])
1081
+ when :amp.ns
1082
+ p1 = regex[:p1]
1083
+
1084
+ return false unless accept_nil?(p1)
1085
+
1086
+ no_ret?(p1, preturn(p1)) ||
1087
+ !invalid?(and_preds(preturn(p1), regex[:predicates]))
1088
+ else
1089
+ raise "Unexpected #{:op.ns} #{regex[:op.ns]}"
1090
+ end
1091
+ end
1092
+
1093
+ def preturn(regex)
1094
+ regex = reg_resolve!(regex)
1095
+ return unless regex?(regex)
1096
+
1097
+ p0, *_pr = regex[:predicates]
1098
+ k, *ks = regex[:keys]
1099
+
1100
+ case regex[:op.ns]
1101
+ when :accept.ns then regex[:return_value]
1102
+ when :pcat.ns then add_ret(p0, regex[:return_value], k)
1103
+ when :rep.ns then add_ret(regex[:p1], regex[:return_value], k)
1104
+ when :amp.ns
1105
+ pret = preturn(regex[:p1])
1106
+
1107
+ if no_ret?(regex[:p1], pret)
1108
+ :nil.ns
1109
+ else
1110
+ and_preds(pret, regex[:predicates])
1111
+ end
1112
+ when :alt.ns
1113
+ ps, ks = filter_alt(regex[:predicates], regex[:keys], &method(:accept_nil?))
1114
+
1115
+ r = if ps.first.nil?
1116
+ :nil.ns
1117
+ else
1118
+ preturn(ps.first)
1119
+ end
1120
+
1121
+ if ks && ks.first
1122
+ [ks.first, r]
1123
+ else
1124
+ r
1125
+ end
1126
+ else
1127
+ raise "Unexpected #{:op.ns} #{regex[:op.ns]}"
1128
+ end
1129
+ end
1130
+
1131
+ def add_ret(regex, r, key)
1132
+ regex = reg_resolve!(regex)
1133
+ return r unless regex?(regex)
1134
+
1135
+ prop = -> do
1136
+ return_value = preturn(regex)
1137
+
1138
+ if return_value.empty?
1139
+ r
1140
+ else
1141
+ val = key ? { key => return_value } : return_value
1142
+
1143
+ regex[:splice] ? Utils.into(r, val) : r.conj(val)
1144
+ end
1145
+ end
1146
+
1147
+ case regex[:op.ns]
1148
+ when :accept.ns, :alt.ns, :amp.ns
1149
+ return_value = preturn(regex)
1150
+
1151
+ if return_value == :nil.ns
1152
+ r
1153
+ else
1154
+ r.conj(key ? { key => return_value } : return_value)
1155
+ end
1156
+ when :pcat.ns, :rep.ns then prop.call
1157
+ else
1158
+ raise "Unexpected #{:op.ns} #{regex[:op.ns]}"
1159
+ end
1160
+ end
1161
+
1162
+ def deriv(predicate, value)
1163
+ predicate = reg_resolve!(predicate)
1164
+ return unless predicate
1165
+
1166
+ unless regex?(predicate)
1167
+ return_value = dt(predicate, value)
1168
+
1169
+ return if invalid?(return_value)
1170
+ return accept(return_value)
1171
+ end
1172
+
1173
+ regex = predicate
1174
+
1175
+ predicates, p1, p2, keys, return_value, splice =
1176
+ regex.values_at(:predicates, :p1, :p2, :keys, :return_value, :splice)
1177
+
1178
+ pred, *rest_preds = predicates
1179
+ key, *rest_keys = keys
1180
+
1181
+ case regex[:op.ns]
1182
+ when :accept.ns then nil
1183
+ when :pcat.ns
1184
+ regex1 = pcat(:predicates => [deriv(pred, value), *rest_preds], :keys => keys, :return_value => return_value)
1185
+ regex2 = nil
1186
+
1187
+ if accept_nil?(pred)
1188
+ regex2 = deriv(
1189
+ pcat(:predicates => rest_preds, :keys => rest_keys, :return_value => add_ret(pred, return_value, key)),
1190
+ value
1191
+ )
1192
+ end
1193
+
1194
+ alt2(regex1, regex2)
1195
+ when :alt.ns
1196
+ _alt(predicates.map { |p| deriv(p, value) }, keys)
1197
+ when :rep.ns
1198
+ regex1 = rep(deriv(p1, value), p2, return_value, splice)
1199
+ regex2 = nil
1200
+
1201
+ if accept_nil?(p1)
1202
+ regex2 = deriv(rep(p2, p2, add_ret(p1, return_value, nil), splice), value)
1203
+ end
1204
+
1205
+ alt2(regex1, regex2)
1206
+ when :amp.ns
1207
+ p1 = deriv(p1, value)
1208
+ return unless p1
1209
+
1210
+ if p1[:op.ns] == :accept.ns
1211
+ ret = and_preds(preturn(p1), predicates)
1212
+ accept(ret) unless invalid?(ret)
1213
+ else
1214
+ constrained(p1, *predicates)
1215
+ end
1216
+ else
1217
+ raise "Unexpected #{:op.ns} #{regex[:op.ns]}"
1218
+ end
1219
+ end
1220
+
1221
+ def insufficient(path, via, inn)
1222
+ [{ :path => path,
1223
+ :reason => "Insufficient input",
1224
+ :val => [],
1225
+ :via => via,
1226
+ :in => inn }]
1227
+ end
1228
+
1229
+ def op_explain(p, path, via, inn, input)
1230
+ p = reg_resolve!(p)
1231
+ return unless p
1232
+
1233
+ input ||= []
1234
+ x = input.first
1235
+
1236
+ unless regex?(p)
1237
+ if input.empty?
1238
+ return insufficient(path, via, inn)
1239
+ else
1240
+ return explain1(p, path, via, inn, x)
1241
+ end
1242
+ end
1243
+
1244
+ case p[:op.ns]
1245
+ when :accept.ns then nil
1246
+ when :amp.ns
1247
+ if input.empty?
1248
+ if accept_nil?(p[:p1])
1249
+ explain_pred_list(p[:predicates], path, via, inn, preturn(p[:p1]))
1250
+ else
1251
+ insufficient(path, via, inn)
1252
+ end
1253
+ else
1254
+ p1 = deriv(p[:p1], x)
1255
+
1256
+ if p1
1257
+ explain_pred_list(p[:predicates], path, via, inn, preturn(p1))
1258
+ else
1259
+ op_explain(p[:p1], path, via, inn, input)
1260
+ end
1261
+ end
1262
+ when :pcat.ns
1263
+ pks = p[:predicates].zip(p[:keys] || [])
1264
+ pred, k = if pks.count == 1
1265
+ pks.first
1266
+ else
1267
+ pks.lazy.reject { |(predicate, _)| accept_nil?(predicate) }.first
1268
+ end
1269
+ path = path.conj(k) if k
1270
+
1271
+ if input.empty? && !pred
1272
+ insufficient(path, via, inn)
1273
+ else
1274
+ op_explain(pred, path, via, inn, input)
1275
+ end
1276
+ when :alt.ns
1277
+ return insufficient(path, via, inn) if input.empty?
1278
+
1279
+ probs = p[:predicates].zip(p[:keys]).flat_map { |(predicate, key)|
1280
+ op_explain(predicate, key ? path.conj(key) : path, via, inn, input)
1281
+ }
1282
+
1283
+ probs.compact
1284
+ when :rep.ns
1285
+ op_explain(p[:p1], path, via, inn, input)
1286
+ else
1287
+ raise "Unexpected #{:op.ns} #{p[:op.ns]}"
1288
+ end
1289
+ end
1290
+
1291
+ # Resets the spec registry to only builtin specs
1292
+ def reset_registry!
1293
+ builtins = {
1294
+ :any.ns => with_gen(Utils.constantly(true)) { |r| r.branch(*Gen::GEN_BUILTINS.values) },
1295
+ :boolean.ns => Set[true, false],
1296
+ :positive_integer.ns => with_gen(self.and(Integer, ->(x) { x > 0 })) { |r| r.range(1) },
1297
+ # Rantly#positive_integer is actually a natural integer
1298
+ :natural_integer.ns => with_gen(self.and(Integer, ->(x) { x >= 0 }), &:positive_integer),
1299
+ :negative_integer.ns => with_gen(self.and(Integer, ->(x) { x < 0 })) { |r| r.range(nil, -1) },
1300
+ :empty.ns => with_gen(:empty?.to_proc) { |_| [] }
1301
+ }
1302
+
1303
+ @registry_ref.reset(builtins)
1304
+ end
1305
+ end
1306
+
1307
+ reset_registry!
1308
+ end