speculation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +87 -0
- data/.travis.yml +16 -0
- data/.yardopts +3 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +29 -0
- data/bin/bundler +17 -0
- data/bin/byebug +17 -0
- data/bin/coderay +17 -0
- data/bin/console +70 -0
- data/bin/cucumber-queue +17 -0
- data/bin/minitest-queue +17 -0
- data/bin/pry +17 -0
- data/bin/rake +17 -0
- data/bin/rspec-queue +17 -0
- data/bin/rubocop +17 -0
- data/bin/ruby-parse +17 -0
- data/bin/ruby-rewrite +17 -0
- data/bin/setup +8 -0
- data/bin/testunit-queue +17 -0
- data/bin/yard +17 -0
- data/bin/yardoc +17 -0
- data/bin/yri +17 -0
- data/lib/speculation/conj.rb +32 -0
- data/lib/speculation/error.rb +17 -0
- data/lib/speculation/gen.rb +106 -0
- data/lib/speculation/identifier.rb +47 -0
- data/lib/speculation/namespaced_symbols.rb +28 -0
- data/lib/speculation/pmap.rb +30 -0
- data/lib/speculation/spec_impl/and_spec.rb +39 -0
- data/lib/speculation/spec_impl/every_spec.rb +176 -0
- data/lib/speculation/spec_impl/f_spec.rb +121 -0
- data/lib/speculation/spec_impl/hash_spec.rb +215 -0
- data/lib/speculation/spec_impl/merge_spec.rb +40 -0
- data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
- data/lib/speculation/spec_impl/or_spec.rb +62 -0
- data/lib/speculation/spec_impl/regex_spec.rb +35 -0
- data/lib/speculation/spec_impl/spec.rb +47 -0
- data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
- data/lib/speculation/spec_impl.rb +36 -0
- data/lib/speculation/test.rb +553 -0
- data/lib/speculation/utils.rb +64 -0
- data/lib/speculation/utils_specs.rb +57 -0
- data/lib/speculation/version.rb +4 -0
- data/lib/speculation.rb +1308 -0
- data/speculation.gemspec +43 -0
- metadata +246 -0
data/lib/speculation.rb
ADDED
@@ -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
|