speculation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|