speculation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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