zizek 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/CHANGELOG.md +115 -0
- data/GUIDE.md +774 -0
- data/LICENSE +201 -0
- data/README.md +201 -0
- data/docs/assets/zizek-rb-logo-dark.svg +16 -0
- data/docs/assets/zizek-rb-logo.svg +16 -0
- data/examples/combinators.rb +93 -0
- data/examples/coverage_assertions.rb +138 -0
- data/examples/database_workflow.rb +120 -0
- data/examples/die_hard.rb +122 -0
- data/examples/hypothesis_quickstart.rb +44 -0
- data/examples/keyed_draws.rb +123 -0
- data/examples/ledger.rb +114 -0
- data/examples/min_stack.rb +108 -0
- data/examples/minitest_suite.rb +64 -0
- data/examples/multi.rb +46 -0
- data/examples/queue.rb +122 -0
- data/examples/roundtrip.rb +71 -0
- data/examples/stack.rb +68 -0
- data/examples/tour.rb +126 -0
- data/lib/zizek/database.rb +211 -0
- data/lib/zizek/gen.rb +10377 -0
- data/lib/zizek/generators.rb +879 -0
- data/lib/zizek/minitest.rb +94 -0
- data/lib/zizek/rspec.rb +67 -0
- data/lib/zizek/settings.rb +249 -0
- data/lib/zizek/shell.rb +2938 -0
- data/lib/zizek/stateful.rb +349 -0
- data/lib/zizek/version.rb +8 -0
- data/lib/zizek.rb +8 -0
- metadata +109 -0
data/lib/zizek/shell.rb
ADDED
|
@@ -0,0 +1,2938 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
require_relative "gen"
|
|
6
|
+
|
|
7
|
+
module Zizek
|
|
8
|
+
class InvalidReplay < ArgumentError; end
|
|
9
|
+
class ReplayOverrun < StandardError; end
|
|
10
|
+
class ChoiceOverrun < StandardError; end
|
|
11
|
+
class AssumptionRejected < StandardError; end
|
|
12
|
+
|
|
13
|
+
# No value could be produced. Raised by Generators::Combinators#example when
|
|
14
|
+
# a filter rejects every attempt; a property reports this as a health check
|
|
15
|
+
# instead, because inside a run a rejection is data about the search.
|
|
16
|
+
class Unsatisfiable < StandardError; end
|
|
17
|
+
|
|
18
|
+
class FailedHealthCheck < StandardError
|
|
19
|
+
attr_reader :check
|
|
20
|
+
|
|
21
|
+
def initialize(check, message)
|
|
22
|
+
@check = check
|
|
23
|
+
super(message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A coverage requirement the completed run missed.
|
|
28
|
+
#
|
|
29
|
+
# There is no counterexample and so no blob or shrinking: the failure is the
|
|
30
|
+
# absence of an example, not an example. The counters are a pure function of
|
|
31
|
+
# the run's choice streams, so rerunning with the same seed reproduces the
|
|
32
|
+
# identical verdict.
|
|
33
|
+
class FailedCoverage < StandardError
|
|
34
|
+
# min_percent is the strongest rate required for the label across its call
|
|
35
|
+
# sites and require entries; zero means "at least once".
|
|
36
|
+
attr_reader :label, :observed, :valid, :min_percent
|
|
37
|
+
|
|
38
|
+
def initialize(label, observed, valid, min_percent)
|
|
39
|
+
@label = label
|
|
40
|
+
@observed = observed
|
|
41
|
+
@valid = valid
|
|
42
|
+
@min_percent = min_percent
|
|
43
|
+
message =
|
|
44
|
+
if min_percent.zero?
|
|
45
|
+
"coverage: \"#{label}\" never held in #{valid} valid examples"
|
|
46
|
+
else
|
|
47
|
+
"coverage: \"#{label}\" held in #{observed} of #{valid} valid " \
|
|
48
|
+
"examples, below the required #{format('%g', min_percent)}%"
|
|
49
|
+
end
|
|
50
|
+
super(message)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class TooManyRejections < StandardError
|
|
55
|
+
attr_reader :rejections, :limit
|
|
56
|
+
|
|
57
|
+
def initialize(rejections, limit)
|
|
58
|
+
@rejections = rejections
|
|
59
|
+
@limit = limit
|
|
60
|
+
super("too many rejected examples: got #{rejections} rejections (limit #{limit})")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class Failure < StandardError
|
|
65
|
+
attr_reader :blob, :property_cause, :examples_run, :rejections, :notes, :values, :seed,
|
|
66
|
+
:puns, :key_misses, :costs
|
|
67
|
+
alias cause property_cause
|
|
68
|
+
|
|
69
|
+
# Keyword-only because there are ten of these and a positional call site
|
|
70
|
+
# said nothing about which zero meant what.
|
|
71
|
+
def initialize(blob:, property_cause:, examples_run:, rejections: 0, notes: [], values: [],
|
|
72
|
+
seed: nil, puns: 0, key_misses: 0, costs: {})
|
|
73
|
+
@blob = blob
|
|
74
|
+
@property_cause = property_cause
|
|
75
|
+
@examples_run = examples_run
|
|
76
|
+
@rejections = rejections
|
|
77
|
+
@notes = notes.freeze
|
|
78
|
+
# The named draws this example made, in draw order. See TestCase#draw:
|
|
79
|
+
# naming is for the report only and records no choice, so a blob replays
|
|
80
|
+
# identically whether or not its draws were named.
|
|
81
|
+
@values = values.freeze
|
|
82
|
+
# The seed this search ran under. The blob is the portable artifact; the
|
|
83
|
+
# seed only repeats this search, in this language and version.
|
|
84
|
+
@seed = seed
|
|
85
|
+
# Replayed draws that could not use their recorded value. See
|
|
86
|
+
# ir/replay.zir: a pun is legal but silent, so it is counted rather than
|
|
87
|
+
# raised.
|
|
88
|
+
@puns = puns
|
|
89
|
+
# Keyed draws whose name the blob did not carry.
|
|
90
|
+
@key_misses = key_misses
|
|
91
|
+
# What the reported example spent.
|
|
92
|
+
@costs = costs.freeze
|
|
93
|
+
super(render)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def render
|
|
99
|
+
lines = []
|
|
100
|
+
unless values.empty?
|
|
101
|
+
arguments = values.map { |name, value| "#{name}=#{value.inspect}" }.join(", ")
|
|
102
|
+
lines << "Falsifying example: (#{arguments})"
|
|
103
|
+
end
|
|
104
|
+
detail = property_cause.message.to_s
|
|
105
|
+
lines << (detail.empty? ? property_cause.class.name : "#{property_cause.class}: #{detail}")
|
|
106
|
+
unless costs.empty?
|
|
107
|
+
lines << "cost: #{costs.map { |label, amount| "#{label}=#{amount}" }.join(", ")}"
|
|
108
|
+
end
|
|
109
|
+
lines << "reproduce with: Zizek.check(property, reproduce: #{blob.inspect})"
|
|
110
|
+
unless key_misses.zero?
|
|
111
|
+
noun = key_misses == 1 ? "draw was" : "draws were"
|
|
112
|
+
lines << "warning: #{key_misses} keyed #{noun} not in this blob and " \
|
|
113
|
+
"used the simplest value instead"
|
|
114
|
+
end
|
|
115
|
+
unless puns.zero?
|
|
116
|
+
# The values above are not the ones the blob recorded. Say so: a
|
|
117
|
+
# punned replay is a different run wearing the same blob.
|
|
118
|
+
noun = puns == 1 ? "choice" : "choices"
|
|
119
|
+
lines << "warning: #{puns} replayed #{noun} did not fit this property " \
|
|
120
|
+
"and fell back to a default; the example above is not the one " \
|
|
121
|
+
"the blob recorded"
|
|
122
|
+
end
|
|
123
|
+
lines << "seed: #{seed}" unless seed.nil?
|
|
124
|
+
rendered = lines.join("\n")
|
|
125
|
+
rendered += "\n\nTrace:\n- #{notes.join("\n- ")}" unless notes.empty?
|
|
126
|
+
rendered
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A failure that did not fail the same way when its own choices were
|
|
131
|
+
# replayed: the property depends on something other than what it drew. There
|
|
132
|
+
# is no shrunk example to trust, so this carries the failure as first found
|
|
133
|
+
# and the blob of the stream that produced it. Unlike a Failure's, that blob
|
|
134
|
+
# need not fail when reproduced.
|
|
135
|
+
class Flaky < StandardError
|
|
136
|
+
attr_reader :blob, :property_cause, :examples_run, :rejections, :seed
|
|
137
|
+
alias cause property_cause
|
|
138
|
+
|
|
139
|
+
def initialize(blob:, property_cause:, examples_run:, rejections: 0, seed: nil)
|
|
140
|
+
@blob = blob
|
|
141
|
+
@property_cause = property_cause
|
|
142
|
+
@examples_run = examples_run
|
|
143
|
+
@rejections = rejections
|
|
144
|
+
@seed = seed
|
|
145
|
+
super(render)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def render
|
|
151
|
+
detail = property_cause.message.to_s
|
|
152
|
+
reason = detail.empty? ? property_cause.class.name : "#{property_cause.class}: #{detail}"
|
|
153
|
+
lines = [
|
|
154
|
+
"flaky test: the property failed, then did not fail the same way when " \
|
|
155
|
+
"that example was replayed",
|
|
156
|
+
"original failure: #{reason}",
|
|
157
|
+
"reproduce with: Zizek.check(property, reproduce: #{blob.inspect})"
|
|
158
|
+
]
|
|
159
|
+
lines << "seed: #{seed}" unless seed.nil?
|
|
160
|
+
lines.join("\n")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# `encoded` stays the inner entry whether or not the draw is named, so
|
|
165
|
+
# indexing, shrinking, and replay resolution never have to know about keys.
|
|
166
|
+
# `child` is the stream a clone draw carries; nil for every other kind. Its
|
|
167
|
+
# entry is built at serialization time rather than at record time, because
|
|
168
|
+
# the child keeps drawing after the clone is recorded.
|
|
169
|
+
Draw = Struct.new(:kind, :encoded, :value, :params, :forced, :key, :child)
|
|
170
|
+
|
|
171
|
+
module Shell
|
|
172
|
+
# A string as the u32-LE codepoints the choice stream records, and back.
|
|
173
|
+
def self.pack_codepoints(value)
|
|
174
|
+
value.codepoints.pack("V*")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.unpack_codepoints(packed)
|
|
178
|
+
packed.unpack("V*").pack("U*")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The stream entry for a draw: its value, wrapped if it has a name.
|
|
182
|
+
def self.entry(draw)
|
|
183
|
+
encoded = draw.encoded
|
|
184
|
+
if draw.kind == :clone
|
|
185
|
+
children = draw.child.nil? ? [] : draw.child.__send__(:draws)
|
|
186
|
+
encoded = Core.serialize_clone_header(children.length) +
|
|
187
|
+
children.map { |child| entry(child) }.join
|
|
188
|
+
end
|
|
189
|
+
return encoded if draw.key.nil?
|
|
190
|
+
|
|
191
|
+
Core.serialize_keyed_choice(draw.key, encoded)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Give a clone a stream of its own, drawn from its parent's, so two streams
|
|
195
|
+
# never hand back the same values. A parent without an RNG (a bare replay,
|
|
196
|
+
# or the all-simplest probe) has none to give.
|
|
197
|
+
def self.spawn_rng(rng)
|
|
198
|
+
rng.nil? ? nil : Random.new(rng.rand(1 << 62))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Count a draw list the way Hegel's flattened length does: a clone
|
|
202
|
+
# contributes itself plus everything inside it.
|
|
203
|
+
def self.flat_draw_count(draws)
|
|
204
|
+
draws.sum { |draw| draw.child.nil? ? 1 : 1 + flat_draw_count(draw.child.__send__(:draws)) }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Hash a draw's name into the 64-bit key a stream records.
|
|
209
|
+
#
|
|
210
|
+
# Components may be strings or integers; the encoding is compiled
|
|
211
|
+
# (ir/keys.zir) so the same name hashes identically in every language.
|
|
212
|
+
def self.key_encode(components)
|
|
213
|
+
encoded = +""
|
|
214
|
+
components.each do |component|
|
|
215
|
+
case component
|
|
216
|
+
when Integer
|
|
217
|
+
encoded = Core.key_append_int(encoded, Shell.signed_bytes(component))
|
|
218
|
+
when String, Symbol
|
|
219
|
+
encoded = Core.key_append_text(encoded, component.to_s.b)
|
|
220
|
+
else
|
|
221
|
+
raise ArgumentError, "key components must be String, Symbol, or Integer"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
encoded
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Use this for a decision the property makes at most once per example. For a
|
|
228
|
+
# decision it makes repeatedly, see TestCase#next_key.
|
|
229
|
+
def self.key_hash(*components)
|
|
230
|
+
Core.key_hash(key_encode(components))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The default `text` alphabet: every Unicode scalar value, as Hypothesis
|
|
234
|
+
# uses. Generation is biased towards the simple end of the shrink order, so
|
|
235
|
+
# most drawn characters are still ASCII (see ir/generation.zir).
|
|
236
|
+
UNICODE = Core.alphabet_range(0, 0x10FFFF)
|
|
237
|
+
|
|
238
|
+
# The alphabet holding exactly these characters.
|
|
239
|
+
#
|
|
240
|
+
# Alphabets are interval sets, canonical by construction: the order the
|
|
241
|
+
# characters are written in and any repeats among them do not change the
|
|
242
|
+
# result, so `alphabet("cab")` and `alphabet("abc")` are the same alphabet
|
|
243
|
+
# and index their members identically.
|
|
244
|
+
def self.alphabet(characters)
|
|
245
|
+
encoded = +""
|
|
246
|
+
characters.each_codepoint do |codepoint|
|
|
247
|
+
unless Core.alphabet_codepoint_allowed(codepoint)
|
|
248
|
+
raise ArgumentError, "U+#{codepoint.to_s(16).upcase} cannot be an alphabet member"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
encoded = Core.alphabet_insert(encoded, codepoint)
|
|
252
|
+
end
|
|
253
|
+
encoded
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The alphabet holding every scalar value in a codepoint range. Surrogates
|
|
257
|
+
# are not scalar values and are dropped from the range rather than rejected,
|
|
258
|
+
# so `alphabet_range(0, 0x10FFFF)` is the whole of Unicode.
|
|
259
|
+
def self.alphabet_range(min_codepoint, max_codepoint)
|
|
260
|
+
Core.alphabet_range(min_codepoint, max_codepoint)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class ChoiceSpan
|
|
264
|
+
attr_accessor :start, :finish, :label, :depth, :parent, :discarded
|
|
265
|
+
|
|
266
|
+
def initialize(start:, finish:, label:, depth:, parent:, discarded: false)
|
|
267
|
+
@start = start
|
|
268
|
+
@finish = finish
|
|
269
|
+
@label = label
|
|
270
|
+
@depth = depth
|
|
271
|
+
@parent = parent
|
|
272
|
+
@discarded = discarded
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
class TestCase
|
|
277
|
+
attr_reader :draws, :spans, :notes, :drawn
|
|
278
|
+
|
|
279
|
+
# How deeply clones may nest. This is Hegel's MAX_CLONE_DEPTH and the wire
|
|
280
|
+
# format's own limit (ir/wire.zir @choice_entry_end): a stream nested
|
|
281
|
+
# deeper than this cannot be validated, so refusing to build one keeps the
|
|
282
|
+
# shell from emitting a blob nothing can read.
|
|
283
|
+
MAX_CLONE_DEPTH = 100
|
|
284
|
+
|
|
285
|
+
def initialize(rng: nil, replay: nil, simplest: false, parent: nil)
|
|
286
|
+
# The stream this one was cloned from; nil for the case the property was
|
|
287
|
+
# handed. Everything a run reports — notes, costs, coverage, puns, the
|
|
288
|
+
# choice budget — belongs to the family rather than to one stream, so
|
|
289
|
+
# those route through #root.
|
|
290
|
+
@parent = parent
|
|
291
|
+
# Every draw in the family, including those inside clones. Kept on the
|
|
292
|
+
# root so the choice budget stays O(1) per draw.
|
|
293
|
+
@flat_draws = 0
|
|
294
|
+
@integer_swarm = nil
|
|
295
|
+
@float_swarm = nil
|
|
296
|
+
@rng = rng
|
|
297
|
+
@replay = replay
|
|
298
|
+
@simplest = simplest
|
|
299
|
+
@position = 4
|
|
300
|
+
@remaining = replay.nil? ? 0 : Core.choice_stream_count(replay)
|
|
301
|
+
@draws = []
|
|
302
|
+
@spans = []
|
|
303
|
+
@span_stack = []
|
|
304
|
+
@notes = []
|
|
305
|
+
# Named draws, in draw order, for the falsifying-example report. Local to
|
|
306
|
+
# this stream rather than the family: a clone reports its own.
|
|
307
|
+
@drawn = []
|
|
308
|
+
@generation_depth = 0
|
|
309
|
+
@generation_us = 0
|
|
310
|
+
# Replayed draws that could not use their recorded value; see
|
|
311
|
+
# ir/replay.zir.
|
|
312
|
+
@puns = 0
|
|
313
|
+
# Identifiers handed out by #fresh_id, family-wide so an identifier is
|
|
314
|
+
# unique across every stream of the case. Only ever grows: that is what
|
|
315
|
+
# keeps the recorded windows monotone within a run, so consuming a value
|
|
316
|
+
# cannot destabilize the identifiers recorded after it.
|
|
317
|
+
@fresh_ids = Set.new
|
|
318
|
+
# Keyed draws whose name the stream did not carry. Like a pun this is a
|
|
319
|
+
# silent substitution — the draw falls back to its simplest value — so it
|
|
320
|
+
# is counted rather than raised.
|
|
321
|
+
@key_misses = 0
|
|
322
|
+
@keyed = nil
|
|
323
|
+
# Per-name ordinals for next_key. Execution-local by construction: a
|
|
324
|
+
# counter that outlives one example produces different keys on the replay
|
|
325
|
+
# than on the run that recorded it, and every draw misses.
|
|
326
|
+
@key_counts = {}
|
|
327
|
+
# What this example spent, per label. The shrinker minimizes the total
|
|
328
|
+
# ahead of the choice stream.
|
|
329
|
+
@costs = {}
|
|
330
|
+
# Targeting observations for this example, per label. The run keeps the
|
|
331
|
+
# best-scoring example per label and hill-climbs it on a schedule.
|
|
332
|
+
@targets = {}
|
|
333
|
+
# Coverage observations buffered for this example. Zizek.check commits
|
|
334
|
+
# them into its run-level counters only when the example turns out
|
|
335
|
+
# valid, so a rejected or overrun example contributes nothing — the
|
|
336
|
+
# same convention discarded draws follow.
|
|
337
|
+
@coverage_order = []
|
|
338
|
+
@coverage_held = {}
|
|
339
|
+
@coverage_permille = {}
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
attr_reader :coverage_order, :coverage_held, :coverage_permille
|
|
343
|
+
|
|
344
|
+
attr_reader :costs
|
|
345
|
+
|
|
346
|
+
attr_reader :targets
|
|
347
|
+
|
|
348
|
+
# The case the property was handed: the family every clone belongs to.
|
|
349
|
+
def root
|
|
350
|
+
case_at = self
|
|
351
|
+
case_at = case_at.parent until case_at.parent.nil?
|
|
352
|
+
case_at
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# How many clones deep this stream sits.
|
|
356
|
+
def depth
|
|
357
|
+
levels = 0
|
|
358
|
+
case_at = self
|
|
359
|
+
until case_at.parent.nil?
|
|
360
|
+
levels += 1
|
|
361
|
+
case_at = case_at.parent
|
|
362
|
+
end
|
|
363
|
+
levels
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# A child test case that draws from a stream of its own.
|
|
367
|
+
#
|
|
368
|
+
# The clone occupies one choice position in this stream and gets its own
|
|
369
|
+
# prefix, RNG, and span structure, so its draws neither shift nor are
|
|
370
|
+
# shifted by the parent's — which is what makes it the seam a concurrent
|
|
371
|
+
# runner would use. Everything the run reports is shared: notes, costs,
|
|
372
|
+
# coverage, and the choice budget all belong to the family.
|
|
373
|
+
#
|
|
374
|
+
# This is Hegel's hegel_test_case_clone without threads. The child is drawn
|
|
375
|
+
# from sequentially, but the wire framing (`[5][u32 child count][children]`)
|
|
376
|
+
# and the replay rules are the engine's, so a blob carrying a clone crosses
|
|
377
|
+
# between the two.
|
|
378
|
+
#
|
|
379
|
+
# On replay a clone value at this position hands the child its recorded
|
|
380
|
+
# stream. Any other value puns to an empty child, which overruns on its
|
|
381
|
+
# first draw in a bare replay and extends randomly under a probe.
|
|
382
|
+
def clone_stream
|
|
383
|
+
check_choice_budget
|
|
384
|
+
# Upstream concludes the family invalid past the limit rather than
|
|
385
|
+
# failing the property, so this is a rejection: a false assume's path.
|
|
386
|
+
raise AssumptionRejected if depth + 1 > MAX_CLONE_DEPTH
|
|
387
|
+
|
|
388
|
+
position, use_simplest = resolve(:clone, nil)
|
|
389
|
+
child = TestCase.new(parent: self)
|
|
390
|
+
if use_simplest
|
|
391
|
+
child.replay_simplest!
|
|
392
|
+
elsif @replay.nil?
|
|
393
|
+
child.replay_random!(Shell.spawn_rng(@rng))
|
|
394
|
+
else
|
|
395
|
+
inner = Core.clone_inner_stream(@replay, position)
|
|
396
|
+
if inner.nil?
|
|
397
|
+
# A non-clone entry where a clone belongs: pun to an empty child.
|
|
398
|
+
root.count_pun!
|
|
399
|
+
inner = Core.choice_list_header(0)
|
|
400
|
+
end
|
|
401
|
+
child.replay_stream!(inner, Shell.spawn_rng(@rng))
|
|
402
|
+
end
|
|
403
|
+
record(Draw.new(:clone, "".b, nil, [], false, nil, child))
|
|
404
|
+
child
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Append a draw and count it against the family's choice budget.
|
|
408
|
+
def record(draw)
|
|
409
|
+
draws << draw
|
|
410
|
+
root.count_draw!
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
protected
|
|
415
|
+
|
|
416
|
+
attr_reader :parent
|
|
417
|
+
|
|
418
|
+
# Every identifier the family has ever drawn. Reached through #root:
|
|
419
|
+
# uniqueness is family-wide rather than per stream.
|
|
420
|
+
attr_reader :fresh_ids
|
|
421
|
+
|
|
422
|
+
def count_draw!
|
|
423
|
+
@flat_draws += 1
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def count_pun!
|
|
427
|
+
@puns += 1
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def count_key_miss!
|
|
431
|
+
@key_misses += 1
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def flat_draws
|
|
435
|
+
@flat_draws
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def replay_simplest!
|
|
439
|
+
@simplest = true
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def replay_random!(rng)
|
|
443
|
+
@rng = rng
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def replay_stream!(stream, rng)
|
|
447
|
+
@replay = stream
|
|
448
|
+
@remaining = Core.choice_stream_count(stream)
|
|
449
|
+
@rng = rng
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
public
|
|
453
|
+
|
|
454
|
+
# Record that this example spent something scarce.
|
|
455
|
+
#
|
|
456
|
+
# The shrinker orders candidates by total cost first and by the choice
|
|
457
|
+
# stream second, so a candidate injecting one fewer fault wins even when its
|
|
458
|
+
# stream is longer. A property that never calls this is ordered as before.
|
|
459
|
+
def cost(label, amount = 1)
|
|
460
|
+
raise ArgumentError, "cost amount must be >= 0" if amount.negative?
|
|
461
|
+
|
|
462
|
+
costs = root.__send__(:costs)
|
|
463
|
+
costs[label] = costs.fetch(label, 0) + amount
|
|
464
|
+
nil
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def total_cost
|
|
468
|
+
root.__send__(:costs).values.sum
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Record a targeting observation: generation is steered towards examples
|
|
472
|
+
# that maximize the score. The run keeps, per label, the best-scoring
|
|
473
|
+
# valid example, and hill-climbs those examples on a schedule between
|
|
474
|
+
# ordinary generation. The score must be finite, and each label may be
|
|
475
|
+
# observed at most once per example — a property targeting several
|
|
476
|
+
# quantities names them apart.
|
|
477
|
+
def target(score, label: "")
|
|
478
|
+
score = Float(score)
|
|
479
|
+
unless score.finite?
|
|
480
|
+
raise ArgumentError, "target(#{score}, label: #{label.inspect}) requires a finite score"
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
targets = root.__send__(:targets)
|
|
484
|
+
if targets.key?(label)
|
|
485
|
+
raise ArgumentError,
|
|
486
|
+
"target(#{score}, label: #{label.inspect}) would overwrite an earlier " \
|
|
487
|
+
"observation; each label can be observed at most once per example"
|
|
488
|
+
end
|
|
489
|
+
targets[label] = score
|
|
490
|
+
nil
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# The key naming the next draw in a sequence.
|
|
494
|
+
#
|
|
495
|
+
# Appends an ordinal counted *within this name*, so `next_key("read", conn)`
|
|
496
|
+
# numbers that connection's reads independently of every other sequence. An
|
|
497
|
+
# insertion then shifts only its own sequence.
|
|
498
|
+
def next_key(*components)
|
|
499
|
+
scope = Zizek.__send__(:key_encode, components)
|
|
500
|
+
ordinal = @key_counts.fetch(scope, 0)
|
|
501
|
+
@key_counts[scope] = ordinal + 1
|
|
502
|
+
Core.key_hash(Core.key_append_int(scope, Shell.signed_bytes(ordinal)))
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
attr_reader :puns, :key_misses
|
|
506
|
+
|
|
507
|
+
# Generation and the simplest probe record protocol 3. The probe used to
|
|
508
|
+
# record the legacy protocol, whose stream the sniffer reads back as
|
|
509
|
+
# protocol 2 whenever the first rule's first draw is an integer, so its
|
|
510
|
+
# failures did not replay.
|
|
511
|
+
def stateful_protocol
|
|
512
|
+
@replay.nil? ? 3 : Core.stateful_choice_protocol(@replay, @position, @remaining)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def stateful_feature_probability_raw(generated)
|
|
516
|
+
@replay.nil? ? forced_integer(generated) : integer(0, 254)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def assume(condition)
|
|
520
|
+
raise AssumptionRejected unless condition
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Fail this example unless condition holds.
|
|
524
|
+
#
|
|
525
|
+
# An ordinary property failure that shrinks and replays like any other,
|
|
526
|
+
# with this call site as the bug's origin. A passing evaluation also
|
|
527
|
+
# counts as covering the label, so a call site that may legitimately
|
|
528
|
+
# never execute is fine on its own ("always or unreachable"), while
|
|
529
|
+
# listing the label in `must_cover:` additionally fails a run that never
|
|
530
|
+
# reached it.
|
|
531
|
+
def always(label, condition)
|
|
532
|
+
raise "always: \"#{label}\" did not hold" unless condition
|
|
533
|
+
|
|
534
|
+
cover(label, true)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Fail this example: the assertion is that nothing reaches this call.
|
|
538
|
+
#
|
|
539
|
+
# The failure shrinks and replays like any other, so the reported
|
|
540
|
+
# counterexample is the simplest input that reaches the branch.
|
|
541
|
+
def unreachable(label)
|
|
542
|
+
raise "unreachable: \"#{label}\" was reached"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Assert that condition holds in at least one valid example.
|
|
546
|
+
#
|
|
547
|
+
# A property passes vacuously when the branch it exists to test never
|
|
548
|
+
# executes; sometimes turns that silent gap into a failure. The verdict
|
|
549
|
+
# is per run, not per example, so a false condition here never fails the
|
|
550
|
+
# current example — only a completed run in which it never held.
|
|
551
|
+
def sometimes(label, condition)
|
|
552
|
+
cover(label, condition)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Assert that condition holds in at least percent% of valid examples.
|
|
556
|
+
#
|
|
557
|
+
# And at least once, whatever the rate. A label counts once per example
|
|
558
|
+
# in which its condition held, however many times the call site ran.
|
|
559
|
+
# Labels a run must observe even when the call site itself may never
|
|
560
|
+
# execute are declared up front via the `must_cover:` option.
|
|
561
|
+
def cover(label, condition, percent: 0)
|
|
562
|
+
unless percent >= 0 && percent <= 100
|
|
563
|
+
raise ArgumentError, "cover percent must be between 0 and 100"
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
family = root
|
|
567
|
+
unless family.__send__(:coverage_held).key?(label)
|
|
568
|
+
family.__send__(:coverage_order) << label
|
|
569
|
+
family.__send__(:coverage_held)[label] = false
|
|
570
|
+
family.__send__(:coverage_permille)[label] = 0
|
|
571
|
+
end
|
|
572
|
+
family.__send__(:coverage_held)[label] = true if condition
|
|
573
|
+
permille = (percent * 10).to_i
|
|
574
|
+
if permille > family.__send__(:coverage_permille)[label]
|
|
575
|
+
family.__send__(:coverage_permille)[label] = permille
|
|
576
|
+
end
|
|
577
|
+
nil
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# Draw from a generator.
|
|
581
|
+
#
|
|
582
|
+
# A named draw is reported in the falsifying example. The name is for the
|
|
583
|
+
# report only: it records no choice, so naming a draw cannot change the
|
|
584
|
+
# blob, the shrink order, or what any other language package replays.
|
|
585
|
+
# Nested draws made by a composite generator stay anonymous, so only the
|
|
586
|
+
# values a property actually names are rendered.
|
|
587
|
+
def draw(generator, name: nil)
|
|
588
|
+
outer = @generation_depth.zero?
|
|
589
|
+
@generation_depth += 1
|
|
590
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) if outer
|
|
591
|
+
begin
|
|
592
|
+
value = generator.do_draw(self)
|
|
593
|
+
drawn << [name, value] unless name.nil?
|
|
594
|
+
value
|
|
595
|
+
ensure
|
|
596
|
+
@generation_depth -= 1
|
|
597
|
+
if outer
|
|
598
|
+
elapsed = (
|
|
599
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - started
|
|
600
|
+
) / 1_000
|
|
601
|
+
@generation_us = [((1 << 64) - 1), @generation_us + elapsed].min
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
attr_accessor :generation_us
|
|
607
|
+
|
|
608
|
+
def note(message)
|
|
609
|
+
root.__send__(:notes) << message.to_s
|
|
610
|
+
nil
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def start_span(label)
|
|
614
|
+
parent = @span_stack.last
|
|
615
|
+
span_id = spans.length
|
|
616
|
+
spans << ChoiceSpan.new(
|
|
617
|
+
start: draws.length,
|
|
618
|
+
finish: draws.length,
|
|
619
|
+
label: label,
|
|
620
|
+
depth: @span_stack.length,
|
|
621
|
+
parent: parent
|
|
622
|
+
)
|
|
623
|
+
@span_stack << span_id
|
|
624
|
+
span_id
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def stop_span(discarded: false)
|
|
628
|
+
return if @span_stack.empty?
|
|
629
|
+
|
|
630
|
+
span = spans[@span_stack.pop]
|
|
631
|
+
span.finish = draws.length
|
|
632
|
+
span.discarded = discarded
|
|
633
|
+
nil
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def open_span_depth
|
|
637
|
+
@span_stack.length
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def reset_open_spans_to(depth, discarded: true)
|
|
641
|
+
stop_span(discarded: discarded) while @span_stack.length > depth
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def close_open_spans
|
|
645
|
+
stop_span until @span_stack.empty?
|
|
646
|
+
# A clone has its own span structure, and the property may leave one open
|
|
647
|
+
# on a child exactly as it may on the stream it was handed.
|
|
648
|
+
draws.each { |draw| draw.child&.__send__(:close_open_spans) }
|
|
649
|
+
nil
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def check_choice_budget
|
|
653
|
+
@replay = nil if @remaining.zero? && !@rng.nil?
|
|
654
|
+
# The budget belongs to the family, not to one stream: upstream's cap is
|
|
655
|
+
# on the whole pieced-together choice sequence, so a clone cannot buy
|
|
656
|
+
# another 8,192 draws by starting a stream of its own.
|
|
657
|
+
if @replay.nil? && Core.choice_budget_exhausted(root.flat_draws)
|
|
658
|
+
raise ChoiceOverrun, "generated example exceeded the choice budget"
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def integer(minimum, maximum, shrink_towards: 0, key: nil, _uniform: false)
|
|
663
|
+
raise ArgumentError, "minimum must be <= maximum" if minimum > maximum
|
|
664
|
+
check_choice_budget
|
|
665
|
+
|
|
666
|
+
position, simplest = resolve("integer", key)
|
|
667
|
+
value =
|
|
668
|
+
if simplest
|
|
669
|
+
Shell.signed_int(
|
|
670
|
+
Core.integer_simplest(
|
|
671
|
+
Shell.signed_bytes(minimum),
|
|
672
|
+
Shell.signed_bytes(maximum),
|
|
673
|
+
Shell.signed_bytes(shrink_towards)
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
elsif @replay.nil?
|
|
677
|
+
if !_uniform && Core.integer_swarm_is_wide(minimum, maximum)
|
|
678
|
+
count = Core.integer_boundary_count(minimum, maximum)
|
|
679
|
+
family = root
|
|
680
|
+
family.instance_variable_set(:@integer_swarm, @rng.rand(1 << 64)) if family.instance_variable_get(:@integer_swarm).nil?
|
|
681
|
+
category = Core.integer_swarm_category(
|
|
682
|
+
family.instance_variable_get(:@integer_swarm), @rng.rand(1000)
|
|
683
|
+
)
|
|
684
|
+
if category.zero?
|
|
685
|
+
Core.integer_swarm_endpoint_candidate(minimum, maximum, @rng.rand(4))
|
|
686
|
+
elsif category == 1 && count > 2
|
|
687
|
+
Core.integer_boundary_candidate(minimum, maximum, 2 + @rng.rand(count - 2))
|
|
688
|
+
elsif category == 2
|
|
689
|
+
Core.integer_swarm_diffuse_candidate(
|
|
690
|
+
minimum, maximum, @rng.rand(Core.integer_swarm_diffuse_count)
|
|
691
|
+
) || @rng.rand(minimum..maximum)
|
|
692
|
+
else
|
|
693
|
+
@rng.rand(minimum..maximum)
|
|
694
|
+
end
|
|
695
|
+
else
|
|
696
|
+
# Narrow ranges are uniform: a boundary boost here over-picked the
|
|
697
|
+
# ends of small ranges, i.e. the first and last option of a list
|
|
698
|
+
# indexed by this draw.
|
|
699
|
+
@rng.rand(minimum..maximum)
|
|
700
|
+
end
|
|
701
|
+
else
|
|
702
|
+
low = Shell.signed_bytes(minimum)
|
|
703
|
+
high = Shell.signed_bytes(maximum)
|
|
704
|
+
@puns += 1 if Core.replay_integer_puns(@replay, position, low, high)
|
|
705
|
+
Shell.signed_int(
|
|
706
|
+
Core.replay_integer(
|
|
707
|
+
@replay, position, low, high, Shell.signed_bytes(shrink_towards)
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
encoded = Core.serialize_integer_choice(Shell.signed_bytes(value))
|
|
713
|
+
record(Draw.new(:integer, encoded, value, [minimum, maximum, shrink_towards], false, key))
|
|
714
|
+
value
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def boolean(key: nil)
|
|
718
|
+
check_choice_budget
|
|
719
|
+
position, simplest = resolve("boolean", key)
|
|
720
|
+
value =
|
|
721
|
+
if simplest
|
|
722
|
+
false
|
|
723
|
+
elsif @replay.nil?
|
|
724
|
+
@rng.rand(2) == 1
|
|
725
|
+
else
|
|
726
|
+
@puns += 1 if Core.replay_boolean_puns(@replay, position)
|
|
727
|
+
Core.replay_boolean(@replay, position)
|
|
728
|
+
end
|
|
729
|
+
record(Draw.new(:boolean, Core.serialize_boolean_choice(value), value, [], false, key))
|
|
730
|
+
value
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Draw an identifier that is arbitrary but unique within this family.
|
|
734
|
+
#
|
|
735
|
+
# The identifier is recorded *by value*, so it keeps meaning the same
|
|
736
|
+
# variable when unrelated earlier draws are deleted during shrinking —
|
|
737
|
+
# which is the whole point of it, and what a positional index cannot do.
|
|
738
|
+
# Generation always hands out the smallest unused identifier, and a
|
|
739
|
+
# replayed identifier that is already in use is repaired to that same
|
|
740
|
+
# smallest unused one.
|
|
741
|
+
#
|
|
742
|
+
# This is Hegel's draw_fresh_id (hegel-c 0.32.4).
|
|
743
|
+
def fresh_id
|
|
744
|
+
used = root.fresh_ids
|
|
745
|
+
window_hi = Core.stateful_fresh_id_window(used.empty? ? 0 : used.max + 1)
|
|
746
|
+
# Ascending order is load-bearing: the fold stops climbing at the first
|
|
747
|
+
# gap, so walking the registry any other way would not answer with the
|
|
748
|
+
# smallest unused identifier.
|
|
749
|
+
fallback = used.sort.reduce(0) do |candidate, used_id|
|
|
750
|
+
Core.stateful_fresh_id_fallback_step(candidate, used_id)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
value = draw_identifier(window_hi, fallback, -> { fallback }) do |raw|
|
|
754
|
+
Core.stateful_fresh_id_accepts(raw, window_hi, used.include?(raw)) ? raw : fallback
|
|
755
|
+
end
|
|
756
|
+
used << value
|
|
757
|
+
value
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# Draw one of members, recording the member itself.
|
|
761
|
+
#
|
|
762
|
+
# Recording the member rather than an index into the live set is what makes
|
|
763
|
+
# a reference survive the deletion of *other* members: while the member it
|
|
764
|
+
# names is still there it keeps naming it, and when that member goes the
|
|
765
|
+
# reference slides to an earlier one rather than to whatever now occupies
|
|
766
|
+
# some shifted position.
|
|
767
|
+
#
|
|
768
|
+
# This is Hegel's draw_from_set (hegel-c 0.32.4).
|
|
769
|
+
def from_set(members)
|
|
770
|
+
raise ArgumentError, "from_set requires at least one member" if members.empty?
|
|
771
|
+
|
|
772
|
+
ordered = members.uniq.sort
|
|
773
|
+
raise ArgumentError, "from_set requires nonnegative members" if ordered.first.negative?
|
|
774
|
+
|
|
775
|
+
smallest = ordered.first
|
|
776
|
+
used = root.fresh_ids
|
|
777
|
+
window_hi = Core.stateful_set_choice_window(used.empty? ? 0 : used.max + 1)
|
|
778
|
+
|
|
779
|
+
draw_identifier(window_hi, smallest, -> { ordered[@rng.rand(ordered.length)] }) do |raw|
|
|
780
|
+
if Core.stateful_set_choice_accepts(raw, window_hi)
|
|
781
|
+
ordered.reduce(smallest) do |best, candidate|
|
|
782
|
+
Core.stateful_set_choice_better(candidate, raw, best) ? candidate : best
|
|
783
|
+
end
|
|
784
|
+
else
|
|
785
|
+
smallest
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Draw a boolean that is true with the given probability. It is recorded as
|
|
791
|
+
# an ordinary boolean choice: the weight biases generation only, so a blob
|
|
792
|
+
# replays the same whatever probability produced it.
|
|
793
|
+
def weighted(probability, key: nil)
|
|
794
|
+
check_choice_budget
|
|
795
|
+
position, simplest = resolve("boolean", key)
|
|
796
|
+
value =
|
|
797
|
+
if simplest
|
|
798
|
+
false
|
|
799
|
+
elsif @replay.nil?
|
|
800
|
+
@rng.rand < probability
|
|
801
|
+
else
|
|
802
|
+
@puns += 1 if Core.replay_boolean_puns(@replay, position)
|
|
803
|
+
Core.replay_boolean(@replay, position)
|
|
804
|
+
end
|
|
805
|
+
record(Draw.new(:boolean, Core.serialize_boolean_choice(value), value, [], false, key))
|
|
806
|
+
value
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
def bytes(minimum = 0, maximum = 16, key: nil)
|
|
810
|
+
raise ArgumentError, "require 0 <= minimum <= maximum" if minimum.negative? || minimum > maximum
|
|
811
|
+
check_choice_budget
|
|
812
|
+
|
|
813
|
+
position, simplest = resolve("bytes", key)
|
|
814
|
+
value =
|
|
815
|
+
if simplest
|
|
816
|
+
Core.bytes_simplest(minimum)
|
|
817
|
+
elsif @replay.nil?
|
|
818
|
+
@rng.bytes(@rng.rand(minimum..maximum))
|
|
819
|
+
else
|
|
820
|
+
@puns += 1 if Core.replay_bytes_puns(@replay, position, minimum, maximum)
|
|
821
|
+
Core.replay_bytes(@replay, position, minimum, maximum)
|
|
822
|
+
end
|
|
823
|
+
record(Draw.new(:bytes, Core.serialize_bytes_choice(value), value, [minimum, maximum], false, key))
|
|
824
|
+
value
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# Draw text of length [minimum, maximum] over an alphabet, by default every
|
|
828
|
+
# Unicode scalar value.
|
|
829
|
+
#
|
|
830
|
+
# Shrinking is shortlex over the alphabet's shrink order, which puts "0"
|
|
831
|
+
# through "Z" first, so a minimal counterexample reads as "0" rather than
|
|
832
|
+
# as a control character.
|
|
833
|
+
def string(minimum = 0, maximum = 16, alphabet: nil, key: nil)
|
|
834
|
+
characters = alphabet || UNICODE
|
|
835
|
+
raise ArgumentError, "require 0 <= minimum <= maximum" if minimum.negative? || minimum > maximum
|
|
836
|
+
unless Core.alphabet_valid(characters)
|
|
837
|
+
raise ArgumentError, "alphabet must be a canonical non-empty interval set"
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
check_choice_budget
|
|
841
|
+
position, simplest = resolve("string", key)
|
|
842
|
+
packed =
|
|
843
|
+
if simplest
|
|
844
|
+
Core.string_simplest(characters, minimum)
|
|
845
|
+
elsif @replay.nil?
|
|
846
|
+
span = Core.text_rank_span(characters, @rng.rand(1000) < Core.text_bias_threshold)
|
|
847
|
+
Array.new(@rng.rand(minimum..maximum)) do
|
|
848
|
+
Core.alphabet_shrink_codepoint(characters, @rng.rand(span))
|
|
849
|
+
end.pack("V*")
|
|
850
|
+
else
|
|
851
|
+
if Core.replay_string_puns(@replay, position, characters, minimum, maximum)
|
|
852
|
+
@puns += 1
|
|
853
|
+
end
|
|
854
|
+
Core.replay_string(@replay, position, characters, minimum, maximum)
|
|
855
|
+
end
|
|
856
|
+
value = Shell.unpack_codepoints(packed)
|
|
857
|
+
record(Draw.new(
|
|
858
|
+
:string,
|
|
859
|
+
Core.serialize_string_choice_packed(packed.bytesize / 4, packed),
|
|
860
|
+
value,
|
|
861
|
+
[characters, minimum, maximum],
|
|
862
|
+
false,
|
|
863
|
+
key
|
|
864
|
+
))
|
|
865
|
+
value
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
# Draw an IEEE-754 binary64 value with portable replay and shrinking.
|
|
869
|
+
# Bounds use sign-aware ordering, so -0.0 is strictly below +0.0.
|
|
870
|
+
def float(
|
|
871
|
+
minimum = -Float::INFINITY,
|
|
872
|
+
maximum = Float::INFINITY,
|
|
873
|
+
allow_nan: false,
|
|
874
|
+
allow_infinity: false,
|
|
875
|
+
smallest_nonzero_magnitude: 0.0,
|
|
876
|
+
key: nil
|
|
877
|
+
)
|
|
878
|
+
unless Core.float_generation_spec_valid(
|
|
879
|
+
minimum, maximum, allow_nan, allow_infinity, smallest_nonzero_magnitude
|
|
880
|
+
)
|
|
881
|
+
raise ArgumentError, "invalid float bounds"
|
|
882
|
+
end
|
|
883
|
+
unit = Core.float_unit(
|
|
884
|
+
minimum, maximum, allow_nan, allow_infinity, smallest_nonzero_magnitude
|
|
885
|
+
)
|
|
886
|
+
raise ArgumentError, "float configuration admits no values" if unit.nil?
|
|
887
|
+
check_choice_budget
|
|
888
|
+
|
|
889
|
+
position, simplest = resolve("float", key)
|
|
890
|
+
value =
|
|
891
|
+
if simplest
|
|
892
|
+
Core.float_simplest(
|
|
893
|
+
minimum, maximum, allow_nan, allow_infinity, smallest_nonzero_magnitude
|
|
894
|
+
)
|
|
895
|
+
elsif @replay.nil?
|
|
896
|
+
# One family-wide category mixture, then one roll per draw. A
|
|
897
|
+
# category the spec rules out yields to the default draw, so its
|
|
898
|
+
# mass is never handed to a neighbouring category.
|
|
899
|
+
family = root
|
|
900
|
+
family.instance_variable_set(:@float_swarm, @rng.rand(1 << 64)) if family.instance_variable_get(:@float_swarm).nil?
|
|
901
|
+
category = Core.float_swarm_category(
|
|
902
|
+
family.instance_variable_get(:@float_swarm), @rng.rand(Core.float_swarm_roll_span)
|
|
903
|
+
)
|
|
904
|
+
words = Array.new(5) { @rng.rand(1 << 64) }
|
|
905
|
+
Core.float_category_candidate(
|
|
906
|
+
category, minimum, maximum, allow_nan, allow_infinity,
|
|
907
|
+
smallest_nonzero_magnitude, *words
|
|
908
|
+
) || Core.float_default_candidate(
|
|
909
|
+
minimum, maximum, allow_nan, allow_infinity,
|
|
910
|
+
smallest_nonzero_magnitude, *Array.new(4) { @rng.rand(1 << 64) }
|
|
911
|
+
)
|
|
912
|
+
else
|
|
913
|
+
if Core.replay_float_puns(
|
|
914
|
+
@replay, position,
|
|
915
|
+
minimum, maximum, allow_nan, allow_infinity,
|
|
916
|
+
smallest_nonzero_magnitude
|
|
917
|
+
)
|
|
918
|
+
@puns += 1
|
|
919
|
+
end
|
|
920
|
+
Core.replay_float(
|
|
921
|
+
@replay, position,
|
|
922
|
+
minimum, maximum, allow_nan, allow_infinity,
|
|
923
|
+
smallest_nonzero_magnitude
|
|
924
|
+
) || unit
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
params = [minimum, maximum, allow_nan, allow_infinity, smallest_nonzero_magnitude]
|
|
928
|
+
record(Draw.new(:float, Core.serialize_float_choice(value), value, params, false, key))
|
|
929
|
+
value
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def forced_integer(value)
|
|
933
|
+
check_choice_budget
|
|
934
|
+
advance_replay("forced integer") unless @replay.nil?
|
|
935
|
+
encoded = Core.serialize_integer_choice(Shell.signed_bytes(value))
|
|
936
|
+
record(Draw.new(:integer, encoded, value, [value, value, value], true, nil))
|
|
937
|
+
value
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def forced_boolean(value)
|
|
941
|
+
check_choice_budget
|
|
942
|
+
advance_replay("forced boolean") unless @replay.nil?
|
|
943
|
+
record(Draw.new(:boolean, Core.serialize_boolean_choice(value), value, [], true, nil))
|
|
944
|
+
value
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
# Draw a variable-length sequence of values from one generator.
|
|
948
|
+
#
|
|
949
|
+
# The length is never a choice of its own. Each iteration draws a single
|
|
950
|
+
# "another element?" boolean inside a span that also holds that element's
|
|
951
|
+
# own choices, which is what makes a collection structurally shrinkable:
|
|
952
|
+
# deleting the span removes exactly one element and leaves the rest of the
|
|
953
|
+
# stream framed correctly. Truncation falls out of the same shape —
|
|
954
|
+
# lowering one of those booleans to false ends the sequence there.
|
|
955
|
+
#
|
|
956
|
+
# The decision itself lives in the core (ir/many.zir); this loop only
|
|
957
|
+
# supplies entropy and records spans.
|
|
958
|
+
def collect(element, min_size = 0, max_size = nil, label = "collection.element")
|
|
959
|
+
raise ArgumentError, "min_size must be >= 0" if min_size.negative?
|
|
960
|
+
|
|
961
|
+
has_max = !max_size.nil?
|
|
962
|
+
bound = has_max ? max_size : 0
|
|
963
|
+
raise ArgumentError, "max_size must be >= min_size" if has_max && bound < min_size
|
|
964
|
+
|
|
965
|
+
probability = Core.length_p_continue(min_size, bound, has_max)
|
|
966
|
+
draws_choice = Core.many_draws_choice(min_size, bound, has_max)
|
|
967
|
+
values = []
|
|
968
|
+
loop do
|
|
969
|
+
start_span(label)
|
|
970
|
+
forced = Core.many_forced_value(false, values.length, min_size, bound, has_max, probability)
|
|
971
|
+
more =
|
|
972
|
+
if !draws_choice
|
|
973
|
+
# Bounds coincide: upstream draws nothing at all here.
|
|
974
|
+
values.length < min_size
|
|
975
|
+
elsif forced.nil?
|
|
976
|
+
weighted(probability)
|
|
977
|
+
else
|
|
978
|
+
forced_boolean(forced)
|
|
979
|
+
end
|
|
980
|
+
unless more
|
|
981
|
+
stop_span(discarded: true)
|
|
982
|
+
return values
|
|
983
|
+
end
|
|
984
|
+
# An open span is closed for us when the execution unwinds.
|
|
985
|
+
values << draw(element)
|
|
986
|
+
stop_span
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
# What a rejected element costs a unique collection. Defined by
|
|
991
|
+
# ir/many.zir::many_reject_action.
|
|
992
|
+
REJECT_RETRY = 0
|
|
993
|
+
REJECT_STOP = 1
|
|
994
|
+
REJECT_INVALID = 2
|
|
995
|
+
|
|
996
|
+
# Draw a variable-length sequence whose elements are all distinct.
|
|
997
|
+
#
|
|
998
|
+
# Same pacing as #collect, with one extra decision layered on: what a
|
|
999
|
+
# duplicate costs. Upstream spells a unique collection as a *filtered*
|
|
1000
|
+
# element draw inside the same "another element?" loop, so this does too.
|
|
1001
|
+
# Each element gets up to +filter_retry_allowed+ candidates, each in its own
|
|
1002
|
+
# span that is discarded when the candidate is already present — the choices
|
|
1003
|
+
# stay in the stream, so a replay walks the same positions, but the shrinker
|
|
1004
|
+
# may delete them freely.
|
|
1005
|
+
#
|
|
1006
|
+
# When every candidate for one element is a duplicate the *element* is
|
|
1007
|
+
# rejected rather than the example, and +many_reject_action+ decides what
|
|
1008
|
+
# that costs: retry, stop the collection where it is, or — when the
|
|
1009
|
+
# collection cannot reach its own min_size — reject the example the way a
|
|
1010
|
+
# failed assumption does. Both rules live in ir/many.zir.
|
|
1011
|
+
#
|
|
1012
|
+
# +key+ maps an element to the value uniqueness is judged on, which is what
|
|
1013
|
+
# separates a set (identity) from a mapping keyed by one field.
|
|
1014
|
+
def collect_unique(element, key = nil, min_size = 0, max_size = nil,
|
|
1015
|
+
label = "collection.element")
|
|
1016
|
+
raise ArgumentError, "min_size must be >= 0" if min_size.negative?
|
|
1017
|
+
|
|
1018
|
+
has_max = !max_size.nil?
|
|
1019
|
+
bound = has_max ? max_size : 0
|
|
1020
|
+
raise ArgumentError, "max_size must be >= min_size" if has_max && bound < min_size
|
|
1021
|
+
|
|
1022
|
+
probability = Core.length_p_continue(min_size, bound, has_max)
|
|
1023
|
+
draws_choice = Core.many_draws_choice(min_size, bound, has_max)
|
|
1024
|
+
values = []
|
|
1025
|
+
seen = {}
|
|
1026
|
+
rejections = 0
|
|
1027
|
+
force_stop = false
|
|
1028
|
+
loop do
|
|
1029
|
+
start_span(label)
|
|
1030
|
+
forced = Core.many_forced_value(force_stop, values.length, min_size, bound, has_max,
|
|
1031
|
+
probability)
|
|
1032
|
+
more =
|
|
1033
|
+
if !draws_choice
|
|
1034
|
+
# Bounds coincide: upstream draws nothing at all here.
|
|
1035
|
+
values.length < min_size
|
|
1036
|
+
elsif forced.nil?
|
|
1037
|
+
weighted(probability)
|
|
1038
|
+
else
|
|
1039
|
+
forced_boolean(forced)
|
|
1040
|
+
end
|
|
1041
|
+
unless more
|
|
1042
|
+
stop_span(discarded: true)
|
|
1043
|
+
return values
|
|
1044
|
+
end
|
|
1045
|
+
accepted = nil
|
|
1046
|
+
attempts = 0
|
|
1047
|
+
while Core.filter_retry_allowed(attempts)
|
|
1048
|
+
start_span("collection.candidate")
|
|
1049
|
+
candidate = draw(element)
|
|
1050
|
+
attempts += 1
|
|
1051
|
+
fingerprint = key.nil? ? candidate : key.call(candidate)
|
|
1052
|
+
if seen.key?(fingerprint)
|
|
1053
|
+
stop_span(discarded: true)
|
|
1054
|
+
next
|
|
1055
|
+
end
|
|
1056
|
+
stop_span
|
|
1057
|
+
accepted = [candidate, fingerprint]
|
|
1058
|
+
break
|
|
1059
|
+
end
|
|
1060
|
+
if accepted.nil?
|
|
1061
|
+
stop_span(discarded: true)
|
|
1062
|
+
rejections += 1
|
|
1063
|
+
case Core.many_reject_action(rejections, values.length, min_size)
|
|
1064
|
+
when REJECT_INVALID then raise AssumptionRejected
|
|
1065
|
+
when REJECT_STOP then force_stop = true
|
|
1066
|
+
end
|
|
1067
|
+
next
|
|
1068
|
+
end
|
|
1069
|
+
candidate, fingerprint = accepted
|
|
1070
|
+
seen[fingerprint] = true
|
|
1071
|
+
values << candidate
|
|
1072
|
+
stop_span
|
|
1073
|
+
end
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
def finish
|
|
1077
|
+
nil
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
def serialized
|
|
1081
|
+
Core.choice_list_header(draws.length) + draws.map { |draw| Shell.entry(draw) }.join
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
# Engine-only operations. Cross-object callers use __send__ explicitly;
|
|
1086
|
+
# properties and custom generators use the public draw/span API.
|
|
1087
|
+
private :draws, :spans, :notes, :drawn,
|
|
1088
|
+
:coverage_order, :coverage_held, :coverage_permille, :costs,
|
|
1089
|
+
:targets, :root, :depth, :record,
|
|
1090
|
+
:total_cost, :stateful_protocol, :stateful_feature_probability_raw, :generation_us,
|
|
1091
|
+
:open_span_depth, :reset_open_spans_to, :close_open_spans, :check_choice_budget,
|
|
1092
|
+
:fresh_id, :from_set, :forced_integer, :forced_boolean,
|
|
1093
|
+
:collect, :collect_unique, :finish, :serialized,
|
|
1094
|
+
:puns, :key_misses, :generation_us=
|
|
1095
|
+
private_constant :MAX_CLONE_DEPTH, :REJECT_RETRY, :REJECT_STOP, :REJECT_INVALID
|
|
1096
|
+
|
|
1097
|
+
private
|
|
1098
|
+
|
|
1099
|
+
# Record one identifier choice for #fresh_id or #from_set: an integer in
|
|
1100
|
+
# [0, window_hi].
|
|
1101
|
+
#
|
|
1102
|
+
# Identifier draws differ from #integer only in how a replayed value is
|
|
1103
|
+
# resolved — an ordinary integer puns to the nearest legal value, while an
|
|
1104
|
+
# identifier is *repaired* onto one the pool still holds — so what reaches
|
|
1105
|
+
# the stream is an ordinary integer entry and nothing downstream (the wire
|
|
1106
|
+
# format, the shrinker, the indexers) has to know these exist. The repaired
|
|
1107
|
+
# value is what gets recorded, which is what lets a replayed stream heal
|
|
1108
|
+
# rather than drift.
|
|
1109
|
+
def draw_identifier(window_hi, simplest_value, generate, &repair)
|
|
1110
|
+
check_choice_budget
|
|
1111
|
+
|
|
1112
|
+
position, simplest = resolve("integer", nil)
|
|
1113
|
+
value =
|
|
1114
|
+
if simplest
|
|
1115
|
+
simplest_value
|
|
1116
|
+
elsif @replay.nil?
|
|
1117
|
+
generate.call
|
|
1118
|
+
else
|
|
1119
|
+
low = Shell.signed_bytes(0)
|
|
1120
|
+
high = Shell.signed_bytes(window_hi)
|
|
1121
|
+
# A recorded value outside the window is *punned*, not repaired.
|
|
1122
|
+
# Repair is for a value that named a member which has since gone, and
|
|
1123
|
+
# it slides towards an earlier one; a value that was never in the
|
|
1124
|
+
# window names nothing, so it falls back to the simplest member the
|
|
1125
|
+
# way every other out-of-range replay value does. Reading
|
|
1126
|
+
# replay_integer first would lose the distinction — it clamps to the
|
|
1127
|
+
# window, which would send a runaway value to the *largest* member
|
|
1128
|
+
# rather than the smallest.
|
|
1129
|
+
if Core.replay_integer_puns(@replay, position, low, high)
|
|
1130
|
+
@puns += 1
|
|
1131
|
+
simplest_value
|
|
1132
|
+
else
|
|
1133
|
+
repair.call(
|
|
1134
|
+
Shell.signed_int(
|
|
1135
|
+
Core.replay_integer(@replay, position, low, high, Shell.signed_bytes(0))
|
|
1136
|
+
)
|
|
1137
|
+
)
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
encoded = Core.serialize_integer_choice(Shell.signed_bytes(value))
|
|
1142
|
+
record(Draw.new(:integer, encoded, value, [0, window_hi, 0], false, nil))
|
|
1143
|
+
value
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Index the replay stream by key, innermost entry offset first.
|
|
1147
|
+
def key_offsets
|
|
1148
|
+
return @keyed unless @keyed.nil?
|
|
1149
|
+
|
|
1150
|
+
offsets = {}
|
|
1151
|
+
position = 4
|
|
1152
|
+
remaining = Core.choice_stream_count(@replay)
|
|
1153
|
+
while remaining.positive?
|
|
1154
|
+
finish = Core.choice_entry_end(@replay, position, 0)
|
|
1155
|
+
break if finish.zero?
|
|
1156
|
+
|
|
1157
|
+
key = Core.keyed_entry_key(@replay, position)
|
|
1158
|
+
unless key.nil?
|
|
1159
|
+
(offsets[key] ||= []) << Core.keyed_entry_inner(@replay, position)
|
|
1160
|
+
end
|
|
1161
|
+
position = finish
|
|
1162
|
+
remaining -= 1
|
|
1163
|
+
end
|
|
1164
|
+
@keyed = offsets
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
# Where this draw's recorded entry sits, or nil to use the simplest value.
|
|
1168
|
+
#
|
|
1169
|
+
# An unkeyed draw takes the next positional entry and overruns when there is
|
|
1170
|
+
# none. A keyed draw takes the next entry recorded under its name and falls
|
|
1171
|
+
# back to the simplest value when the stream has none — a missing name is an
|
|
1172
|
+
# edit to the property, not a malformed blob.
|
|
1173
|
+
def replay_position(kind, key)
|
|
1174
|
+
return advance_replay(kind) if key.nil?
|
|
1175
|
+
|
|
1176
|
+
pending = key_offsets[key]
|
|
1177
|
+
if pending.nil? || pending.empty?
|
|
1178
|
+
root.count_key_miss!
|
|
1179
|
+
return nil
|
|
1180
|
+
end
|
|
1181
|
+
pending.shift
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
# Where to read this draw from, and whether to use its simplest value.
|
|
1185
|
+
def resolve(kind, key)
|
|
1186
|
+
return [nil, true] if @simplest
|
|
1187
|
+
return [nil, false] if @replay.nil?
|
|
1188
|
+
|
|
1189
|
+
position = replay_position(kind, key)
|
|
1190
|
+
[position, position.nil?]
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
def advance_replay(kind)
|
|
1194
|
+
loop do
|
|
1195
|
+
raise ReplayOverrun, "replay ended before #{kind} draw" if @replay.nil? || @remaining.zero?
|
|
1196
|
+
|
|
1197
|
+
position = @position
|
|
1198
|
+
ending = Core.choice_entry_end(@replay, position, 0)
|
|
1199
|
+
raise InvalidReplay, "invalid replay entry at offset #{position}" if ending.zero?
|
|
1200
|
+
|
|
1201
|
+
@position = ending
|
|
1202
|
+
@remaining -= 1
|
|
1203
|
+
# A named entry belongs to the draw that names it, wherever that draw
|
|
1204
|
+
# happens to fall. The positional cursor steps over it, so adding or
|
|
1205
|
+
# removing keyed draws cannot shift unkeyed ones.
|
|
1206
|
+
return position if Core.keyed_entry_key(@replay, position).nil?
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
end
|
|
1210
|
+
|
|
1211
|
+
module Shell
|
|
1212
|
+
module_function
|
|
1213
|
+
|
|
1214
|
+
def signed_bytes(value)
|
|
1215
|
+
width = 1
|
|
1216
|
+
width += 1 until value >= -(1 << (width * 8 - 1)) && value < (1 << (width * 8 - 1))
|
|
1217
|
+
unsigned = value.negative? ? (1 << (width * 8)) + value : value
|
|
1218
|
+
Array.new(width) { |index| (unsigned >> (index * 8)) & 0xff }.pack("C*")
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
def signed_int(value)
|
|
1222
|
+
return 0 if value.empty?
|
|
1223
|
+
|
|
1224
|
+
unsigned = value.bytes.each_with_index.sum { |byte, index| byte << (index * 8) }
|
|
1225
|
+
bits = value.bytesize * 8
|
|
1226
|
+
value.getbyte(-1) >= 0x80 ? unsigned - (1 << bits) : unsigned
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def execute(property, rng: nil, replay: nil, simplest: false)
|
|
1230
|
+
test_case = TestCase.new(rng: rng, replay: replay, simplest: simplest)
|
|
1231
|
+
begin
|
|
1232
|
+
result = property.call(test_case)
|
|
1233
|
+
test_case.__send__(:finish)
|
|
1234
|
+
raise RuntimeError, "property returned false" if result == false
|
|
1235
|
+
[nil, test_case]
|
|
1236
|
+
rescue Interrupt, SystemExit
|
|
1237
|
+
raise
|
|
1238
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
1239
|
+
[error, test_case]
|
|
1240
|
+
ensure
|
|
1241
|
+
test_case.__send__(:close_open_spans)
|
|
1242
|
+
end
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
# A schedule slot that maps to no legal value. Distinct from the schedule
|
|
1246
|
+
# ending, because the search should step over it rather than give up on
|
|
1247
|
+
# the choice.
|
|
1248
|
+
SCHEDULE_MISS = Object.new
|
|
1249
|
+
|
|
1250
|
+
# The compiled schedule is defined over u64 indexes; byte draws can sit
|
|
1251
|
+
# beyond that, and saturating keeps their earliest (most useful)
|
|
1252
|
+
# candidates reachable.
|
|
1253
|
+
U64_MAX = (1 << 64) - 1
|
|
1254
|
+
|
|
1255
|
+
# The choice's unconstrained index for shortlex comparison.
|
|
1256
|
+
def draw_sort_index(draw)
|
|
1257
|
+
case draw.kind
|
|
1258
|
+
when :boolean
|
|
1259
|
+
Core.boolean_to_index(draw.value)
|
|
1260
|
+
when :integer
|
|
1261
|
+
minimum, maximum, target = draw.params
|
|
1262
|
+
bounds = [minimum, maximum, target].map { |value| signed_bytes(value) }
|
|
1263
|
+
Core.integer_to_index(*bounds, signed_bytes(draw.value))
|
|
1264
|
+
when :bytes
|
|
1265
|
+
minimum, = draw.params
|
|
1266
|
+
Core.bytes_to_index(minimum, draw.value)
|
|
1267
|
+
when :string
|
|
1268
|
+
characters, minimum, = draw.params
|
|
1269
|
+
Core.string_to_index(characters, minimum, Shell.pack_codepoints(draw.value))
|
|
1270
|
+
when :float
|
|
1271
|
+
Core.float_choice_to_index(*draw.params, draw.value)
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
# Where a draw currently sits in its own shrink schedule.
|
|
1276
|
+
def draw_index(draw)
|
|
1277
|
+
return nil if draw.forced
|
|
1278
|
+
|
|
1279
|
+
encoded = draw_sort_index(draw)
|
|
1280
|
+
encoded.nil? ? nil : [signed_int(encoded), U64_MAX].min
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
# The draw kinds the targeting hill climber can step. Strings have no
|
|
1284
|
+
# sensible "one larger", clones are streams rather than values, and a
|
|
1285
|
+
# forced draw is checked separately.
|
|
1286
|
+
def climbable_draw?(draw)
|
|
1287
|
+
!draw.forced && draw.child.nil? &&
|
|
1288
|
+
[:integer, :boolean, :float, :bytes].include?(draw.kind)
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
# The draw stepped by delta, or nil when the stepped value is not
|
|
1292
|
+
# representable within the draw's own constraints. Which values are
|
|
1293
|
+
# reachable is compiled (ir/targeting.zir @targeting_step_*); this only
|
|
1294
|
+
# re-encodes the answer as a stream entry.
|
|
1295
|
+
def step_draw(draw, delta)
|
|
1296
|
+
case draw.kind
|
|
1297
|
+
when :integer
|
|
1298
|
+
minimum, maximum, = draw.params
|
|
1299
|
+
stepped = Core.targeting_step_integer(
|
|
1300
|
+
signed_bytes(minimum), signed_bytes(maximum), signed_bytes(draw.value), delta
|
|
1301
|
+
)
|
|
1302
|
+
return nil if stepped.nil?
|
|
1303
|
+
|
|
1304
|
+
Draw.new(:integer, Core.serialize_integer_choice(stepped), signed_int(stepped),
|
|
1305
|
+
draw.params, false, draw.key)
|
|
1306
|
+
when :boolean
|
|
1307
|
+
stepped = Core.targeting_step_boolean(draw.value, delta)
|
|
1308
|
+
return nil if stepped.nil?
|
|
1309
|
+
|
|
1310
|
+
Draw.new(:boolean, Core.serialize_boolean_choice(stepped), stepped, [], false, draw.key)
|
|
1311
|
+
when :float
|
|
1312
|
+
stepped = Core.targeting_step_float(*draw.params, draw.value, delta)
|
|
1313
|
+
return nil if stepped.nil?
|
|
1314
|
+
|
|
1315
|
+
Draw.new(:float, Core.serialize_float_choice(stepped), stepped, draw.params, false,
|
|
1316
|
+
draw.key)
|
|
1317
|
+
when :bytes
|
|
1318
|
+
minimum, maximum = draw.params
|
|
1319
|
+
stepped = Core.targeting_step_bytes(minimum, maximum, draw.value, delta)
|
|
1320
|
+
return nil if stepped.nil?
|
|
1321
|
+
|
|
1322
|
+
Draw.new(:bytes, Core.serialize_bytes_choice(stepped), stepped, draw.params, false,
|
|
1323
|
+
draw.key)
|
|
1324
|
+
end
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
# An integer draw at the ordinal-th extra lowering candidate, or nil.
|
|
1328
|
+
#
|
|
1329
|
+
# The index schedule bisects, which finds the edge of an interval; a sparse
|
|
1330
|
+
# failing set such as the positive multiples of 1000 is not one, and from
|
|
1331
|
+
# 10**9 the bisection stops at 875000. The divisions and trailing-zeros
|
|
1332
|
+
# moves that carry on from there are compiled (@integer_shrink_extra); this
|
|
1333
|
+
# only re-encodes the value they choose.
|
|
1334
|
+
def integer_extra_draw(draw, ordinal)
|
|
1335
|
+
return nil if draw.kind != :integer || draw.forced
|
|
1336
|
+
|
|
1337
|
+
bounds = draw.params.map { |value| signed_bytes(value) }
|
|
1338
|
+
value = Core.integer_shrink_extra(*bounds, signed_bytes(draw.value), ordinal)
|
|
1339
|
+
return nil if value.nil?
|
|
1340
|
+
|
|
1341
|
+
Draw.new(:integer, Core.serialize_integer_choice(value), signed_int(value), draw.params,
|
|
1342
|
+
false, draw.key)
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
# Where a draw sits and the largest index it could move to, for the raise
|
|
1346
|
+
# pass, as [current, largest]; nil for a draw the pass leaves alone.
|
|
1347
|
+
#
|
|
1348
|
+
# Upstream raises every node but a sequence, so strings and byte strings
|
|
1349
|
+
# are skipped — their index bundles length and content, and a larger one is
|
|
1350
|
+
# a longer value — and so are clones, which carry a stream rather than a
|
|
1351
|
+
# value, and forced draws, which are not this stream's to change.
|
|
1352
|
+
def raise_bounds(draw)
|
|
1353
|
+
return nil if draw.forced || !draw.child.nil?
|
|
1354
|
+
|
|
1355
|
+
largest =
|
|
1356
|
+
case draw.kind
|
|
1357
|
+
when :boolean then Core.boolean_max_index
|
|
1358
|
+
when :integer
|
|
1359
|
+
minimum, maximum, = draw.params
|
|
1360
|
+
Core.integer_max_index(signed_bytes(minimum), signed_bytes(maximum))
|
|
1361
|
+
when :float then Core.float_choice_max_index
|
|
1362
|
+
end
|
|
1363
|
+
return nil if largest.nil?
|
|
1364
|
+
|
|
1365
|
+
current = draw_index(draw)
|
|
1366
|
+
return nil if current.nil?
|
|
1367
|
+
|
|
1368
|
+
[current, [signed_int(largest), U64_MAX].min]
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
# What makes two draws duplicates of each other.
|
|
1372
|
+
#
|
|
1373
|
+
# Upstream groups by (choice type, value) and lowers the group using the
|
|
1374
|
+
# first member's constraints, accepting that a member with different
|
|
1375
|
+
# constraints may reject the shared value. Grouping by the *index* instead
|
|
1376
|
+
# is the same idea in the coordinate the schedule already walks: two draws
|
|
1377
|
+
# with equal bounds share an index exactly when they share a value, and a
|
|
1378
|
+
# member whose bounds differ is rebuilt for its own bounds or misses. A
|
|
1379
|
+
# forced draw is never a member — it has no index and may not be rewritten.
|
|
1380
|
+
def draw_group_key(draw)
|
|
1381
|
+
index = draw_index(draw)
|
|
1382
|
+
index.nil? ? nil : [draw.kind, index]
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
# Rebuild every choice in a span at its simplest value, and report how many
|
|
1386
|
+
# of them the pass was actually able to move.
|
|
1387
|
+
#
|
|
1388
|
+
# This is upstream's try_trivial_spans. Per-choice descent walks one draw at
|
|
1389
|
+
# a time and stops the moment a candidate stops failing, so a span whose
|
|
1390
|
+
# choices only fail *together* at their simplest values is out of its reach
|
|
1391
|
+
# — and the all-simplest probe does not help either, because it collapses
|
|
1392
|
+
# the whole example rather than one span of it. A forced draw is left alone,
|
|
1393
|
+
# as in every other pass, and a draw already at index 0 is copied rather
|
|
1394
|
+
# than rebuilt.
|
|
1395
|
+
def trivial_span_candidate(draws, span)
|
|
1396
|
+
trial = draws.dup
|
|
1397
|
+
rewritable = 0
|
|
1398
|
+
(span.start...[span.finish, draws.length].min).each do |at|
|
|
1399
|
+
index = draw_index(draws[at])
|
|
1400
|
+
next if index.nil? || index.zero?
|
|
1401
|
+
|
|
1402
|
+
candidate = draw_at_index(draws[at], 0)
|
|
1403
|
+
next if candidate.equal?(SCHEDULE_MISS)
|
|
1404
|
+
|
|
1405
|
+
trial[at] = candidate
|
|
1406
|
+
rewritable += 1
|
|
1407
|
+
end
|
|
1408
|
+
[trial, rewritable]
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
# A string draw with the character at one position removed.
|
|
1412
|
+
#
|
|
1413
|
+
# The whole-value index bundles length and content into one number, and the
|
|
1414
|
+
# downward search over it converges on the wrong end of the interval: from
|
|
1415
|
+
# "abb" over an "abcdef" alphabet it probes eleven candidates and never
|
|
1416
|
+
# reaches "bb", which sits far below. Removing a character is the candidate
|
|
1417
|
+
# that search cannot produce.
|
|
1418
|
+
def shorten_string(draw, position)
|
|
1419
|
+
return nil unless draw.kind == :string
|
|
1420
|
+
return nil if draw.forced
|
|
1421
|
+
|
|
1422
|
+
_characters, min_size, = draw.params
|
|
1423
|
+
value = Core.string_drop_character(Shell.pack_codepoints(draw.value), min_size, position)
|
|
1424
|
+
return nil if value.nil?
|
|
1425
|
+
|
|
1426
|
+
candidate = draw.dup
|
|
1427
|
+
candidate.value = Shell.unpack_codepoints(value)
|
|
1428
|
+
candidate.encoded = Core.serialize_string_choice_packed(value.length / 4, value)
|
|
1429
|
+
candidate
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
# A string draw with every copy of one repeated character lowered.
|
|
1433
|
+
#
|
|
1434
|
+
# A string draw carries a single whole-value index, so the per-choice
|
|
1435
|
+
# schedule can only slide the entire value along one linear order. A
|
|
1436
|
+
# property that depends on two *characters* of that value agreeing is the
|
|
1437
|
+
# case it cannot serve — M12's shape, one level down and inside a single
|
|
1438
|
+
# draw.
|
|
1439
|
+
def lower_characters(draw, ordinal)
|
|
1440
|
+
return nil unless draw.kind == :string
|
|
1441
|
+
return nil if draw.forced
|
|
1442
|
+
|
|
1443
|
+
characters, = draw.params
|
|
1444
|
+
packed = Shell.pack_codepoints(draw.value)
|
|
1445
|
+
rank = Core.string_duplicate_rank(characters, packed)
|
|
1446
|
+
return nil if rank.nil?
|
|
1447
|
+
|
|
1448
|
+
target = Core.shrink_candidate_at(rank, ordinal)
|
|
1449
|
+
return nil if target.nil?
|
|
1450
|
+
|
|
1451
|
+
value = Core.string_replace_rank(characters, packed, rank, target)
|
|
1452
|
+
return nil if value.nil?
|
|
1453
|
+
|
|
1454
|
+
candidate = draw.dup
|
|
1455
|
+
candidate.value = Shell.unpack_codepoints(value)
|
|
1456
|
+
candidate.encoded = Core.serialize_string_choice_packed(value.length / 4, value)
|
|
1457
|
+
candidate
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
# The ordinal-th span strictly nested inside spans[at].
|
|
1461
|
+
#
|
|
1462
|
+
# Upstream's pass_to_descendant picks a random descendant; a cursor machine
|
|
1463
|
+
# has no chooser, so the ancestor is the cursor and the descendant is the
|
|
1464
|
+
# ordinal. Spans are recorded in the order they open, so scanning forward
|
|
1465
|
+
# from the ancestor gives a stable enumeration in every language.
|
|
1466
|
+
def descendant_span(spans, at, ordinal)
|
|
1467
|
+
ancestor = spans[at]
|
|
1468
|
+
return nil if ancestor.discarded || ancestor.finish <= ancestor.start
|
|
1469
|
+
|
|
1470
|
+
seen = 0
|
|
1471
|
+
((at + 1)...spans.length).each do |other|
|
|
1472
|
+
candidate = spans[other]
|
|
1473
|
+
next if candidate.start < ancestor.start || candidate.finish > ancestor.finish
|
|
1474
|
+
next if candidate.discarded || candidate.depth <= ancestor.depth
|
|
1475
|
+
next unless Core.shrink_descendant_worthwhile(
|
|
1476
|
+
ancestor.finish - ancestor.start, candidate.finish - candidate.start
|
|
1477
|
+
)
|
|
1478
|
+
return candidate if seen == ordinal
|
|
1479
|
+
|
|
1480
|
+
seen += 1
|
|
1481
|
+
end
|
|
1482
|
+
nil
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
# The span that should trade places with spans[at].
|
|
1486
|
+
#
|
|
1487
|
+
# Upstream groups a span's children by label and sorts the group; taking
|
|
1488
|
+
# only the *next* member makes each request one adjacent swap, and
|
|
1489
|
+
# repeating it until nothing moves sorts the group the same way. A
|
|
1490
|
+
# discarded span is never a member: its choices are a rejected candidate's
|
|
1491
|
+
# debris, so its position carries no meaning to canonicalize.
|
|
1492
|
+
# The choices a paid deletion of spans[at] removes, as [start, finish,
|
|
1493
|
+
# widened], or nil.
|
|
1494
|
+
#
|
|
1495
|
+
# A span of two or more choices is deleted exactly, as the deletion pass
|
|
1496
|
+
# already tried on its own. A single-choice span takes upstream's extent:
|
|
1497
|
+
# the span with the spanless choice before it, or else stretched to the
|
|
1498
|
+
# start of the next span. A discarded span is never a candidate — its
|
|
1499
|
+
# choices are a rejected candidate's debris or a collection's stop marker,
|
|
1500
|
+
# and paying for their deletion buys nothing — and neither is an extent
|
|
1501
|
+
# covering the whole stream.
|
|
1502
|
+
def paid_extent(draws, spans, at)
|
|
1503
|
+
span = spans[at]
|
|
1504
|
+
return nil if span.discarded || span.finish <= span.start || span.finish > draws.length
|
|
1505
|
+
|
|
1506
|
+
start = span.start
|
|
1507
|
+
finish = span.finish
|
|
1508
|
+
widened = false
|
|
1509
|
+
if finish - start < 2
|
|
1510
|
+
widened = true
|
|
1511
|
+
preceded = start.positive? && spans.none? { |other| other.finish == start && other.start < start }
|
|
1512
|
+
if preceded
|
|
1513
|
+
start -= 1
|
|
1514
|
+
else
|
|
1515
|
+
finish = spans.map(&:start).select { |other| other >= span.finish }.min || draws.length
|
|
1516
|
+
return nil if finish <= span.finish
|
|
1517
|
+
end
|
|
1518
|
+
end
|
|
1519
|
+
return nil if start.zero? && finish == draws.length
|
|
1520
|
+
|
|
1521
|
+
[start, finish, widened]
|
|
1522
|
+
end
|
|
1523
|
+
|
|
1524
|
+
# The draw that pays for deleting spans[at]: the first one after the
|
|
1525
|
+
# structure enclosing it, or nil.
|
|
1526
|
+
#
|
|
1527
|
+
# Upstream takes the innermost ancestor span reaching past the deletion. A
|
|
1528
|
+
# zizek collection opens no span of its own — its elements are a run of
|
|
1529
|
+
# adjacent sibling spans sharing a label, ending in the discarded span of
|
|
1530
|
+
# its final "no more" choice — so that run is what plays the part of the
|
|
1531
|
+
# list, and only a span outside any such run falls back to its ancestors.
|
|
1532
|
+
def paid_follower(spans, at, finish, draw_count)
|
|
1533
|
+
span = spans[at]
|
|
1534
|
+
run_end = span.finish
|
|
1535
|
+
extended = false
|
|
1536
|
+
((at + 1)...spans.length).each do |other|
|
|
1537
|
+
candidate = spans[other]
|
|
1538
|
+
next if candidate.parent != span.parent
|
|
1539
|
+
break if candidate.label != span.label || candidate.start != run_end
|
|
1540
|
+
|
|
1541
|
+
run_end = candidate.finish
|
|
1542
|
+
extended = true
|
|
1543
|
+
end
|
|
1544
|
+
boundary = nil
|
|
1545
|
+
if extended
|
|
1546
|
+
boundary = run_end
|
|
1547
|
+
else
|
|
1548
|
+
parent = span.parent
|
|
1549
|
+
until parent.nil?
|
|
1550
|
+
if spans[parent].finish > finish
|
|
1551
|
+
boundary = spans[parent].finish
|
|
1552
|
+
break
|
|
1553
|
+
end
|
|
1554
|
+
parent = spans[parent].parent
|
|
1555
|
+
end
|
|
1556
|
+
end
|
|
1557
|
+
return nil if boundary.nil? || boundary < finish || boundary >= draw_count
|
|
1558
|
+
|
|
1559
|
+
boundary
|
|
1560
|
+
end
|
|
1561
|
+
|
|
1562
|
+
# The paying draw moved one step: an integer down or up, a boolean to the
|
|
1563
|
+
# side the step names, a string one character shorter. SCHEDULE_MISS when
|
|
1564
|
+
# this draw cannot take this step, so the pass moves to the next one
|
|
1565
|
+
# without running the property.
|
|
1566
|
+
def nudge_draw(draw, step)
|
|
1567
|
+
return SCHEDULE_MISS if draw.forced
|
|
1568
|
+
|
|
1569
|
+
stepped =
|
|
1570
|
+
case draw.kind
|
|
1571
|
+
when :integer
|
|
1572
|
+
step_draw(draw, step)
|
|
1573
|
+
when :boolean
|
|
1574
|
+
candidate = step_draw(draw, step)
|
|
1575
|
+
candidate.nil? || candidate.value == draw.value ? nil : candidate
|
|
1576
|
+
when :string
|
|
1577
|
+
step.negative? ? shorten_string(draw, draw.value.length - 1) : nil
|
|
1578
|
+
end
|
|
1579
|
+
stepped.nil? ? SCHEDULE_MISS : stepped
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
def next_sibling(spans, at)
|
|
1583
|
+
span = spans[at]
|
|
1584
|
+
return nil if span.discarded || span.finish <= span.start
|
|
1585
|
+
|
|
1586
|
+
((at + 1)...spans.length).each do |other|
|
|
1587
|
+
candidate = spans[other]
|
|
1588
|
+
next if candidate.discarded || candidate.parent != span.parent
|
|
1589
|
+
next if candidate.label != span.label
|
|
1590
|
+
next if candidate.finish <= candidate.start || candidate.start < span.finish
|
|
1591
|
+
|
|
1592
|
+
return other
|
|
1593
|
+
end
|
|
1594
|
+
nil
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
# Where a draw sits in its schedule, for the adjacent-pair pass.
|
|
1598
|
+
#
|
|
1599
|
+
# Upstream's lower_integers_together takes integer nodes only, and so does
|
|
1600
|
+
# this: the shift it makes is meaningful in value space precisely because
|
|
1601
|
+
# an integer's index is its distance from its own shrink target. A byte
|
|
1602
|
+
# string's index is a shortlex position, and shifting two of those by a
|
|
1603
|
+
# shared amount preserves nothing a property could depend on.
|
|
1604
|
+
def pair_index(draw)
|
|
1605
|
+
draw.kind == :integer ? draw_index(draw) : nil
|
|
1606
|
+
end
|
|
1607
|
+
|
|
1608
|
+
# Choice-count-first shortlex: shorter streams win, then the first lower
|
|
1609
|
+
# per-choice index wins.
|
|
1610
|
+
def draws_simpler?(candidate, current)
|
|
1611
|
+
draws_compare(candidate, current).negative?
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
# Order two draw lists: negative when candidate is the simpler of the two.
|
|
1615
|
+
#
|
|
1616
|
+
# The count that decides first is the *flattened* one, so a candidate that
|
|
1617
|
+
# removes a choice from inside a clone is simpler even though the parent's
|
|
1618
|
+
# own entry count has not moved — without that, a nested shrink could never
|
|
1619
|
+
# make progress the accept rule would keep. A clone is then compared by its
|
|
1620
|
+
# own stream, recursively, which is the same rule one level down.
|
|
1621
|
+
def draws_compare(candidate, current)
|
|
1622
|
+
left = Shell.flat_draw_count(candidate)
|
|
1623
|
+
right = Shell.flat_draw_count(current)
|
|
1624
|
+
return left < right ? -1 : 1 unless left == right
|
|
1625
|
+
unless candidate.length == current.length
|
|
1626
|
+
return candidate.length < current.length ? -1 : 1
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
candidate.zip(current).each do |candidate_draw, current_draw|
|
|
1630
|
+
if !candidate_draw.child.nil? || !current_draw.child.nil?
|
|
1631
|
+
return 1 if candidate_draw.child.nil? || current_draw.child.nil?
|
|
1632
|
+
|
|
1633
|
+
order = draws_compare(candidate_draw.child.__send__(:draws), current_draw.child.__send__(:draws))
|
|
1634
|
+
return order unless order.zero?
|
|
1635
|
+
|
|
1636
|
+
next
|
|
1637
|
+
end
|
|
1638
|
+
candidate_index = draw_sort_index(candidate_draw)
|
|
1639
|
+
current_index = draw_sort_index(current_draw)
|
|
1640
|
+
return 1 if candidate_index.nil? || current_index.nil?
|
|
1641
|
+
|
|
1642
|
+
order = Core.signed_compare_le(candidate_index, current_index)
|
|
1643
|
+
return order unless order.zero?
|
|
1644
|
+
end
|
|
1645
|
+
0
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
# Rebuild a draw at a schedule index, or report a miss.
|
|
1649
|
+
#
|
|
1650
|
+
# The rebuilt draw keeps its key: a lowered candidate that lost its name
|
|
1651
|
+
# would be looked up positionally on the next execution, so every candidate
|
|
1652
|
+
# for a keyed draw would be rejected and nothing would shrink.
|
|
1653
|
+
def draw_at_index(draw, index)
|
|
1654
|
+
case draw.kind
|
|
1655
|
+
when :boolean
|
|
1656
|
+
value = Core.boolean_from_index(signed_bytes(index))
|
|
1657
|
+
return SCHEDULE_MISS if value.nil?
|
|
1658
|
+
|
|
1659
|
+
Draw.new(:boolean, Core.serialize_boolean_choice(value), value, draw.params, false, draw.key)
|
|
1660
|
+
when :integer
|
|
1661
|
+
minimum, maximum, target = draw.params
|
|
1662
|
+
bounds = [minimum, maximum, target].map { |value| signed_bytes(value) }
|
|
1663
|
+
value_bytes = Core.integer_from_index(*bounds, signed_bytes(index))
|
|
1664
|
+
return SCHEDULE_MISS if value_bytes.nil?
|
|
1665
|
+
|
|
1666
|
+
Draw.new(
|
|
1667
|
+
:integer,
|
|
1668
|
+
Core.serialize_integer_choice(value_bytes),
|
|
1669
|
+
signed_int(value_bytes),
|
|
1670
|
+
draw.params,
|
|
1671
|
+
false,
|
|
1672
|
+
draw.key
|
|
1673
|
+
)
|
|
1674
|
+
when :bytes
|
|
1675
|
+
minimum, maximum = draw.params
|
|
1676
|
+
value = Core.bytes_from_index(minimum, maximum, signed_bytes(index))
|
|
1677
|
+
return SCHEDULE_MISS if value.nil?
|
|
1678
|
+
|
|
1679
|
+
Draw.new(:bytes, Core.serialize_bytes_choice(value), value, draw.params, false, draw.key)
|
|
1680
|
+
when :string
|
|
1681
|
+
characters, minimum, maximum = draw.params
|
|
1682
|
+
packed = Core.string_from_index(characters, minimum, maximum, signed_bytes(index))
|
|
1683
|
+
return SCHEDULE_MISS if packed.nil?
|
|
1684
|
+
|
|
1685
|
+
Draw.new(
|
|
1686
|
+
:string,
|
|
1687
|
+
Core.serialize_string_choice_packed(packed.bytesize / 4, packed),
|
|
1688
|
+
Shell.unpack_codepoints(packed),
|
|
1689
|
+
draw.params,
|
|
1690
|
+
false,
|
|
1691
|
+
draw.key
|
|
1692
|
+
)
|
|
1693
|
+
when :float
|
|
1694
|
+
value = Core.float_choice_from_index(*draw.params, signed_bytes(index))
|
|
1695
|
+
return SCHEDULE_MISS if value.nil?
|
|
1696
|
+
|
|
1697
|
+
Draw.new(:float, Core.serialize_float_choice(value), value, draw.params, false, draw.key)
|
|
1698
|
+
else
|
|
1699
|
+
SCHEDULE_MISS
|
|
1700
|
+
end
|
|
1701
|
+
end
|
|
1702
|
+
|
|
1703
|
+
# Every shrink candidate for a draw, in schedule order.
|
|
1704
|
+
def candidate_draws(draw)
|
|
1705
|
+
current = draw_index(draw)
|
|
1706
|
+
return [] if current.nil?
|
|
1707
|
+
|
|
1708
|
+
candidates = []
|
|
1709
|
+
ordinal = 0
|
|
1710
|
+
loop do
|
|
1711
|
+
index = Core.shrink_candidate_at(current, ordinal)
|
|
1712
|
+
break if index.nil?
|
|
1713
|
+
|
|
1714
|
+
candidate = draw_at_index(draw, index)
|
|
1715
|
+
candidates << candidate unless candidate.equal?(SCHEDULE_MISS)
|
|
1716
|
+
ordinal += 1
|
|
1717
|
+
end
|
|
1718
|
+
candidates
|
|
1719
|
+
end
|
|
1720
|
+
|
|
1721
|
+
def serialized_draws(draws)
|
|
1722
|
+
Core.choice_list_header(draws.length) + draws.map { |draw| Shell.entry(draw) }.join
|
|
1723
|
+
end
|
|
1724
|
+
|
|
1725
|
+
class ChoiceTree
|
|
1726
|
+
def initialize
|
|
1727
|
+
@terminal = false
|
|
1728
|
+
@children = {}
|
|
1729
|
+
end
|
|
1730
|
+
|
|
1731
|
+
def include?(path)
|
|
1732
|
+
node = self
|
|
1733
|
+
path.each_byte do |byte|
|
|
1734
|
+
node = node.child(byte)
|
|
1735
|
+
return false if node.nil?
|
|
1736
|
+
end
|
|
1737
|
+
node.terminal?
|
|
1738
|
+
end
|
|
1739
|
+
|
|
1740
|
+
def insert(path)
|
|
1741
|
+
node = self
|
|
1742
|
+
path.each_byte { |byte| node = node.child_or_insert(byte) }
|
|
1743
|
+
node.mark_terminal
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
protected
|
|
1747
|
+
|
|
1748
|
+
def child(byte)
|
|
1749
|
+
@children[byte]
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
def child_or_insert(byte)
|
|
1753
|
+
@children[byte] ||= ChoiceTree.new
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
def terminal?
|
|
1757
|
+
@terminal
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
def mark_terminal
|
|
1761
|
+
@terminal = true
|
|
1762
|
+
end
|
|
1763
|
+
end
|
|
1764
|
+
|
|
1765
|
+
def span_mutation_candidates(draws, spans, rng)
|
|
1766
|
+
return [] if draws.any? { |draw| !draw.key.nil? }
|
|
1767
|
+
|
|
1768
|
+
grouped = Hash.new { |hash, label| hash[label] = [] }
|
|
1769
|
+
spans.each do |span|
|
|
1770
|
+
if !span.discarded && span.finish > span.start && span.finish <= draws.length
|
|
1771
|
+
grouped[span.label] << [span.start, span.finish]
|
|
1772
|
+
end
|
|
1773
|
+
end
|
|
1774
|
+
original = serialized_draws(draws)
|
|
1775
|
+
candidates = []
|
|
1776
|
+
labels = grouped.keys.select { |label| grouped[label].uniq.length >= 2 }.sort
|
|
1777
|
+
return [] if labels.empty?
|
|
1778
|
+
|
|
1779
|
+
Core.mutation_attempt_count.times do |attempt|
|
|
1780
|
+
randomized = Core.mutation_attempt_randomized(attempt)
|
|
1781
|
+
group_index = randomized ? Core.mutation_index(labels.length, rng.rand(1 << 64)) : 0
|
|
1782
|
+
break if group_index.nil?
|
|
1783
|
+
|
|
1784
|
+
label = labels[group_index]
|
|
1785
|
+
ranges = grouped[label].uniq.sort
|
|
1786
|
+
first = randomized ? Core.mutation_index(ranges.length, rng.rand(1 << 64)) : 0
|
|
1787
|
+
second = randomized ? Core.mutation_distinct_index(ranges.length, first, rng.rand(1 << 64)) : 1
|
|
1788
|
+
next if second.nil?
|
|
1789
|
+
|
|
1790
|
+
a, b = ranges[first], ranges[second]
|
|
1791
|
+
a, b = b, a if (a <=> b).positive?
|
|
1792
|
+
a_start, a_end = a
|
|
1793
|
+
b_start, b_end = b
|
|
1794
|
+
raw = Core.choice_mutate_ranges(
|
|
1795
|
+
original, a_start, a_end, b_start, b_end,
|
|
1796
|
+
Core.mutation_use_first(randomized ? rng.rand(1 << 64) : attempt)
|
|
1797
|
+
)
|
|
1798
|
+
next if raw.nil? || raw == original || candidates.include?(raw)
|
|
1799
|
+
|
|
1800
|
+
candidates << raw
|
|
1801
|
+
end
|
|
1802
|
+
candidates
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
# One label's run-level coverage. Declared requirements are registered
|
|
1806
|
+
# before the first example and observed-only labels in first-observation
|
|
1807
|
+
# order, which is also the order verdicts are checked in — so shells that
|
|
1808
|
+
# agree on the counters agree on the label a failure names.
|
|
1809
|
+
CoverageCounter = Struct.new(:label, :permille, :observed)
|
|
1810
|
+
|
|
1811
|
+
# Outcome tags are defined by ir/lifecycle.zir. Deriving one from a host
|
|
1812
|
+
# exception is the shell's job; deciding what it means is the core's.
|
|
1813
|
+
OUTCOME_VALID = 0
|
|
1814
|
+
OUTCOME_REJECTED = 1
|
|
1815
|
+
OUTCOME_OVERRUN = 2
|
|
1816
|
+
OUTCOME_INVALID = 3
|
|
1817
|
+
OUTCOME_INTERESTING = 4
|
|
1818
|
+
|
|
1819
|
+
def classify(error)
|
|
1820
|
+
return OUTCOME_VALID if error.nil?
|
|
1821
|
+
return OUTCOME_REJECTED if error.is_a?(AssumptionRejected)
|
|
1822
|
+
return OUTCOME_OVERRUN if error.is_a?(ReplayOverrun) || error.is_a?(ChoiceOverrun)
|
|
1823
|
+
return OUTCOME_INVALID if error.is_a?(InvalidReplay)
|
|
1824
|
+
|
|
1825
|
+
OUTCOME_INTERESTING
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1828
|
+
# The backtrace prefix of a frame inside this shell.
|
|
1829
|
+
SHELL_FRAME = "#{__FILE__}:"
|
|
1830
|
+
|
|
1831
|
+
# Identify which bug an exception represents: its class and where it was
|
|
1832
|
+
# raised, never its message, since a message that quotes a drawn value
|
|
1833
|
+
# would change on every shrink step and stop the search immediately.
|
|
1834
|
+
#
|
|
1835
|
+
# The raise site is the innermost frame outside this shell: an assertion
|
|
1836
|
+
# the shell raises on the property's behalf — always, unreachable — would
|
|
1837
|
+
# otherwise be identified by the shell's one raise line, which every such
|
|
1838
|
+
# assertion in the property would share, and the shrinker would migrate
|
|
1839
|
+
# between them.
|
|
1840
|
+
def interesting_origin(error)
|
|
1841
|
+
backtrace = error.backtrace
|
|
1842
|
+
site = backtrace&.find { |frame| !frame.start_with?(SHELL_FRAME) } || backtrace&.first
|
|
1843
|
+
[error.class, site]
|
|
1844
|
+
end
|
|
1845
|
+
|
|
1846
|
+
# Whether an outcome is a failure of the bug being shrunk. Accepting any
|
|
1847
|
+
# failure lets the search migrate to an unrelated bug that happens to be
|
|
1848
|
+
# easier to reach, and then report a minimal counterexample for a bug the
|
|
1849
|
+
# run never found.
|
|
1850
|
+
def failure?(error, origin)
|
|
1851
|
+
outcome = classify(error)
|
|
1852
|
+
return false unless Core.outcome_is_failure(outcome)
|
|
1853
|
+
|
|
1854
|
+
matches = origin.nil? || interesting_origin(error) == origin
|
|
1855
|
+
Core.shrink_accept(outcome, matches)
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
# Internal control flow: the shrinker replayed an example that had failed
|
|
1859
|
+
# and it did not fail the same way. The run reports it as Flaky.
|
|
1860
|
+
class FlakyReplay < StandardError; end
|
|
1861
|
+
|
|
1862
|
+
# Request kinds and feedback codes are defined by ir/lifecycle.zir.
|
|
1863
|
+
PHASE_BASELINE = 0
|
|
1864
|
+
|
|
1865
|
+
REQUEST_DONE = 0
|
|
1866
|
+
REQUEST_BASELINE = 1
|
|
1867
|
+
REQUEST_DELETE_SPAN = 2
|
|
1868
|
+
REQUEST_LOWER = 3
|
|
1869
|
+
REQUEST_VERIFY = 4
|
|
1870
|
+
REQUEST_LOWER_DUPLICATES = 5
|
|
1871
|
+
REQUEST_LOWER_PAIR = 6
|
|
1872
|
+
REQUEST_REORDER = 7
|
|
1873
|
+
REQUEST_TRIVIAL_SPAN = 8
|
|
1874
|
+
REQUEST_DESCENDANT = 9
|
|
1875
|
+
REQUEST_LOWER_CHARACTERS = 10
|
|
1876
|
+
REQUEST_SHORTEN_STRING = 11
|
|
1877
|
+
REQUEST_DELETE_PAID = 12
|
|
1878
|
+
REQUEST_RAISE = 13
|
|
1879
|
+
|
|
1880
|
+
FEEDBACK_SKIP = 0
|
|
1881
|
+
FEEDBACK_REJECTED = 1
|
|
1882
|
+
FEEDBACK_ACCEPTED = 2
|
|
1883
|
+
|
|
1884
|
+
# Serve the generated shrink schedule until it reports done.
|
|
1885
|
+
#
|
|
1886
|
+
# The search order lives in the core; this loop only runs the property,
|
|
1887
|
+
# maps the host's outcome onto a feedback code, and turns a request into
|
|
1888
|
+
# a concrete candidate. A zero cap leaves the search unbounded.
|
|
1889
|
+
# One execution of a candidate: the whole family it produced, and the case
|
|
1890
|
+
# whose draws the candidate stream became. The two differ only when the
|
|
1891
|
+
# level being shrunk is a clone's stream rather than the family's own.
|
|
1892
|
+
ShrinkRun = Struct.new(:family, :subject, :error)
|
|
1893
|
+
|
|
1894
|
+
# The level that shrinks the stream the property was handed.
|
|
1895
|
+
def family_level(property)
|
|
1896
|
+
lambda do |candidate|
|
|
1897
|
+
error, test_case = execute(property, replay: candidate)
|
|
1898
|
+
ShrinkRun.new(test_case, test_case, error)
|
|
1899
|
+
end
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1902
|
+
# Serve the generated shrink schedule over one level until it reports done.
|
|
1903
|
+
# The last value is how many shrinks the sweep accepted, which is what its
|
|
1904
|
+
# cap counts.
|
|
1905
|
+
def shrink_stream(property, level, family, subject, origin = nil, max_shrinks: 0, max_stall: 0)
|
|
1906
|
+
draws = subject.__send__(:draws).dup
|
|
1907
|
+
spans = subject.__send__(:spans).dup
|
|
1908
|
+
family_draws = family.__send__(:draws).dup
|
|
1909
|
+
cost = family.__send__(:total_cost)
|
|
1910
|
+
best = family
|
|
1911
|
+
best_subject = subject
|
|
1912
|
+
accepted = false
|
|
1913
|
+
verified = nil
|
|
1914
|
+
schedule_at = -1
|
|
1915
|
+
schedule_index = nil
|
|
1916
|
+
schedule_count = 0
|
|
1917
|
+
|
|
1918
|
+
adopt = lambda do |run|
|
|
1919
|
+
# accepted means *improved*, not merely adopted: the baseline replays
|
|
1920
|
+
# the example the caller already had, and a caller that treated that as
|
|
1921
|
+
# progress would descend into the same clone forever.
|
|
1922
|
+
if run.family.__send__(:total_cost) < cost || draws_compare(run.family.__send__(:draws), family_draws).negative?
|
|
1923
|
+
accepted = true
|
|
1924
|
+
end
|
|
1925
|
+
draws = run.subject.__send__(:draws).dup
|
|
1926
|
+
spans = run.subject.__send__(:spans).dup
|
|
1927
|
+
family_draws = run.family.__send__(:draws).dup
|
|
1928
|
+
cost = run.family.__send__(:total_cost)
|
|
1929
|
+
best = run.family
|
|
1930
|
+
best_subject = run.subject
|
|
1931
|
+
schedule_at = -1
|
|
1932
|
+
end
|
|
1933
|
+
|
|
1934
|
+
phase = PHASE_BASELINE
|
|
1935
|
+
span_cursor = duplicate_cursor = position = pair_cursor = reorder_cursor = 0
|
|
1936
|
+
trivial_cursor = descendant_cursor = character_cursor = shorten_cursor = 0
|
|
1937
|
+
paid_cursor = raise_cursor = ordinal = shrinks = stall = 0
|
|
1938
|
+
loop do
|
|
1939
|
+
kind = Core.shrink_request_kind(
|
|
1940
|
+
phase, span_cursor, duplicate_cursor, position, pair_cursor,
|
|
1941
|
+
reorder_cursor, trivial_cursor, descendant_cursor, character_cursor,
|
|
1942
|
+
shorten_cursor, paid_cursor, raise_cursor, draws.length, spans.length
|
|
1943
|
+
)
|
|
1944
|
+
break if kind == REQUEST_DONE
|
|
1945
|
+
|
|
1946
|
+
feedback = FEEDBACK_SKIP
|
|
1947
|
+
case kind
|
|
1948
|
+
when REQUEST_BASELINE
|
|
1949
|
+
# The baseline replays a stream that has already failed — the
|
|
1950
|
+
# example as found, or a candidate that failed when it was adopted —
|
|
1951
|
+
# so one that does not fail now is the property, not the search.
|
|
1952
|
+
run = level.call(serialized_draws(draws))
|
|
1953
|
+
raise FlakyReplay unless failure?(run.error, origin)
|
|
1954
|
+
|
|
1955
|
+
adopt.call(run)
|
|
1956
|
+
feedback = FEEDBACK_ACCEPTED
|
|
1957
|
+
when REQUEST_DELETE_SPAN
|
|
1958
|
+
span = spans[Core.shrink_request_span(span_cursor, spans.length)]
|
|
1959
|
+
candidate = nil
|
|
1960
|
+
if span.finish > span.start
|
|
1961
|
+
candidate = Core.choice_delete_range(serialized_draws(draws), span.start, span.finish)
|
|
1962
|
+
end
|
|
1963
|
+
unless candidate.nil?
|
|
1964
|
+
run = level.call(candidate)
|
|
1965
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
1966
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
1967
|
+
)
|
|
1968
|
+
adopt.call(run)
|
|
1969
|
+
feedback = FEEDBACK_ACCEPTED
|
|
1970
|
+
else
|
|
1971
|
+
feedback = FEEDBACK_REJECTED
|
|
1972
|
+
end
|
|
1973
|
+
end
|
|
1974
|
+
when REQUEST_DELETE_PAID
|
|
1975
|
+
# A deletion the plain pass could not make, paid for by moving the
|
|
1976
|
+
# draw after the enclosing structure one step: a list's declared
|
|
1977
|
+
# length, an index into it, a parity flag drawn after it.
|
|
1978
|
+
at = Core.shrink_request_span(paid_cursor, spans.length)
|
|
1979
|
+
start, finish, widened = paid_extent(draws, spans, at)
|
|
1980
|
+
step = nil
|
|
1981
|
+
unless start.nil?
|
|
1982
|
+
follower = paid_follower(spans, at, finish, draws.length)
|
|
1983
|
+
step = Core.shrink_delete_paid_variant(ordinal, widened) if !follower.nil? || widened
|
|
1984
|
+
end
|
|
1985
|
+
unless step.nil?
|
|
1986
|
+
trial = draws[0...start] + draws[finish..]
|
|
1987
|
+
nudged = nil
|
|
1988
|
+
unless step.zero?
|
|
1989
|
+
nudged = follower.nil? ? SCHEDULE_MISS : nudge_draw(draws[follower], step)
|
|
1990
|
+
end
|
|
1991
|
+
if nudged.equal?(SCHEDULE_MISS)
|
|
1992
|
+
# This draw cannot take this step; try the next one without
|
|
1993
|
+
# running the property.
|
|
1994
|
+
feedback = FEEDBACK_REJECTED
|
|
1995
|
+
else
|
|
1996
|
+
trial[follower - (finish - start)] = nudged unless nudged.nil?
|
|
1997
|
+
run = level.call(serialized_draws(trial))
|
|
1998
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
1999
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2000
|
+
)
|
|
2001
|
+
adopt.call(run)
|
|
2002
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2003
|
+
else
|
|
2004
|
+
feedback = FEEDBACK_REJECTED
|
|
2005
|
+
end
|
|
2006
|
+
end
|
|
2007
|
+
end
|
|
2008
|
+
when REQUEST_LOWER
|
|
2009
|
+
if position != schedule_at
|
|
2010
|
+
schedule_at = position
|
|
2011
|
+
schedule_index = draw_index(draws[position])
|
|
2012
|
+
schedule_count = schedule_index.nil? ? 0 : Core.shrink_candidate_count(schedule_index)
|
|
2013
|
+
end
|
|
2014
|
+
candidate = nil
|
|
2015
|
+
unless schedule_index.nil?
|
|
2016
|
+
index = Core.shrink_candidate_at(schedule_index, ordinal)
|
|
2017
|
+
candidate =
|
|
2018
|
+
if index.nil?
|
|
2019
|
+
# Past the index schedule an integer still has the value-space
|
|
2020
|
+
# moves bisection cannot make.
|
|
2021
|
+
integer_extra_draw(draws[position], ordinal - schedule_count)
|
|
2022
|
+
else
|
|
2023
|
+
draw_at_index(draws[position], index)
|
|
2024
|
+
end
|
|
2025
|
+
end
|
|
2026
|
+
unless candidate.nil?
|
|
2027
|
+
if candidate.equal?(SCHEDULE_MISS)
|
|
2028
|
+
# No legal value here; step the schedule without running.
|
|
2029
|
+
feedback = FEEDBACK_REJECTED
|
|
2030
|
+
else
|
|
2031
|
+
trial = draws.dup
|
|
2032
|
+
trial[position] = candidate
|
|
2033
|
+
run = level.call(serialized_draws(trial))
|
|
2034
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2035
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2036
|
+
)
|
|
2037
|
+
adopt.call(run)
|
|
2038
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2039
|
+
else
|
|
2040
|
+
feedback = FEEDBACK_REJECTED
|
|
2041
|
+
end
|
|
2042
|
+
end
|
|
2043
|
+
end
|
|
2044
|
+
when REQUEST_LOWER_DUPLICATES
|
|
2045
|
+
# Every draw that currently sits where this one does, lowered as one.
|
|
2046
|
+
# Neither member can move alone when the property depends on them
|
|
2047
|
+
# being equal: dropping either breaks the failure, so the per-choice
|
|
2048
|
+
# pass rejects every candidate and leaves the pair where it was.
|
|
2049
|
+
group_key = draw_group_key(draws[duplicate_cursor])
|
|
2050
|
+
members =
|
|
2051
|
+
if group_key.nil?
|
|
2052
|
+
[]
|
|
2053
|
+
else
|
|
2054
|
+
(0...draws.length).select { |at| draw_group_key(draws[at]) == group_key }
|
|
2055
|
+
end
|
|
2056
|
+
# One position drives each group, so a group of n is tried once and
|
|
2057
|
+
# not n times.
|
|
2058
|
+
leads = !members.empty? && members.first == duplicate_cursor
|
|
2059
|
+
index =
|
|
2060
|
+
if leads && Core.shrink_duplicate_worthwhile(members.length, group_key[1])
|
|
2061
|
+
Core.shrink_candidate_at(group_key[1], ordinal)
|
|
2062
|
+
end
|
|
2063
|
+
unless index.nil?
|
|
2064
|
+
trial = draws.dup
|
|
2065
|
+
missed = false
|
|
2066
|
+
members.each do |at|
|
|
2067
|
+
candidate = draw_at_index(draws[at], index)
|
|
2068
|
+
if candidate.equal?(SCHEDULE_MISS)
|
|
2069
|
+
missed = true
|
|
2070
|
+
break
|
|
2071
|
+
end
|
|
2072
|
+
trial[at] = candidate
|
|
2073
|
+
end
|
|
2074
|
+
if missed
|
|
2075
|
+
# No legal value for some member; step the schedule without
|
|
2076
|
+
# running the property.
|
|
2077
|
+
feedback = FEEDBACK_REJECTED
|
|
2078
|
+
else
|
|
2079
|
+
run = level.call(serialized_draws(trial))
|
|
2080
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2081
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2082
|
+
)
|
|
2083
|
+
adopt.call(run)
|
|
2084
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2085
|
+
else
|
|
2086
|
+
feedback = FEEDBACK_REJECTED
|
|
2087
|
+
end
|
|
2088
|
+
end
|
|
2089
|
+
end
|
|
2090
|
+
when REQUEST_LOWER_PAIR
|
|
2091
|
+
# Two adjacent integer draws, lowered by one shared amount. This is
|
|
2092
|
+
# the difference-preserving move: a property that fails on x-y==5
|
|
2093
|
+
# rejects every candidate the per-choice pass offers, because
|
|
2094
|
+
# lowering either member alone changes the difference.
|
|
2095
|
+
members = [pair_cursor, pair_cursor + 1]
|
|
2096
|
+
indexes = members.map { |at| pair_index(draws[at]) }
|
|
2097
|
+
base = 0
|
|
2098
|
+
index =
|
|
2099
|
+
if indexes.none?(&:nil?) && Core.shrink_pair_worthwhile(indexes[0], indexes[1])
|
|
2100
|
+
base = Core.shrink_pair_base(indexes[0], indexes[1])
|
|
2101
|
+
Core.shrink_candidate_at(base, ordinal)
|
|
2102
|
+
end
|
|
2103
|
+
unless index.nil?
|
|
2104
|
+
trial = draws.dup
|
|
2105
|
+
missed = false
|
|
2106
|
+
members.zip(indexes).each do |at, current|
|
|
2107
|
+
shifted = Core.shrink_pair_candidate(current, base, index)
|
|
2108
|
+
if shifted.nil?
|
|
2109
|
+
missed = true
|
|
2110
|
+
break
|
|
2111
|
+
end
|
|
2112
|
+
candidate = draw_at_index(draws[at], shifted)
|
|
2113
|
+
if candidate.equal?(SCHEDULE_MISS)
|
|
2114
|
+
missed = true
|
|
2115
|
+
break
|
|
2116
|
+
end
|
|
2117
|
+
trial[at] = candidate
|
|
2118
|
+
end
|
|
2119
|
+
if missed
|
|
2120
|
+
# No legal value for one member; step the schedule without
|
|
2121
|
+
# running the property.
|
|
2122
|
+
feedback = FEEDBACK_REJECTED
|
|
2123
|
+
else
|
|
2124
|
+
run = level.call(serialized_draws(trial))
|
|
2125
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2126
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2127
|
+
)
|
|
2128
|
+
adopt.call(run)
|
|
2129
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2130
|
+
else
|
|
2131
|
+
feedback = FEEDBACK_REJECTED
|
|
2132
|
+
end
|
|
2133
|
+
end
|
|
2134
|
+
end
|
|
2135
|
+
when REQUEST_TRIVIAL_SPAN
|
|
2136
|
+
# A whole span at its simplest values, in one execution.
|
|
2137
|
+
span = spans[trivial_cursor]
|
|
2138
|
+
unless span.discarded || span.finish <= span.start
|
|
2139
|
+
trial, rewritable = trivial_span_candidate(draws, span)
|
|
2140
|
+
if Core.shrink_trivial_worthwhile(rewritable)
|
|
2141
|
+
run = level.call(serialized_draws(trial))
|
|
2142
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2143
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2144
|
+
)
|
|
2145
|
+
adopt.call(run)
|
|
2146
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2147
|
+
else
|
|
2148
|
+
feedback = FEEDBACK_REJECTED
|
|
2149
|
+
end
|
|
2150
|
+
end
|
|
2151
|
+
end
|
|
2152
|
+
when REQUEST_DESCENDANT
|
|
2153
|
+
# A span replaced by one of its own descendants. Deletion removes a
|
|
2154
|
+
# whole subtree; this keeps the subtree and discards the structure
|
|
2155
|
+
# wrapped around it, which is the move a nested failure needs when the
|
|
2156
|
+
# interesting part is buried rather than beside something.
|
|
2157
|
+
inner = descendant_span(spans, descendant_cursor, ordinal)
|
|
2158
|
+
candidate =
|
|
2159
|
+
unless inner.nil?
|
|
2160
|
+
outer = spans[descendant_cursor]
|
|
2161
|
+
# Two deletions rather than a new primitive: drop the tail after
|
|
2162
|
+
# the descendant, then the head before it. Trailing first, so the
|
|
2163
|
+
# leading offsets are still the ones measured above.
|
|
2164
|
+
after = Core.choice_delete_range(
|
|
2165
|
+
serialized_draws(draws), inner.finish, outer.finish
|
|
2166
|
+
)
|
|
2167
|
+
Core.choice_delete_range(after, outer.start, inner.start) unless after.nil?
|
|
2168
|
+
end
|
|
2169
|
+
unless candidate.nil?
|
|
2170
|
+
run = level.call(candidate)
|
|
2171
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2172
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2173
|
+
)
|
|
2174
|
+
adopt.call(run)
|
|
2175
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2176
|
+
else
|
|
2177
|
+
feedback = FEEDBACK_REJECTED
|
|
2178
|
+
end
|
|
2179
|
+
end
|
|
2180
|
+
when REQUEST_REORDER
|
|
2181
|
+
# Two sibling spans trade places. Nothing about the example changes
|
|
2182
|
+
# except which order its parts were drawn in, so this reports a
|
|
2183
|
+
# canonical arrangement rather than the one generation stumbled on.
|
|
2184
|
+
sibling = next_sibling(spans, reorder_cursor)
|
|
2185
|
+
candidate =
|
|
2186
|
+
unless sibling.nil?
|
|
2187
|
+
first = spans[reorder_cursor]
|
|
2188
|
+
second = spans[sibling]
|
|
2189
|
+
Core.choice_swap_ranges(
|
|
2190
|
+
serialized_draws(draws), first.start, first.finish,
|
|
2191
|
+
second.start, second.finish
|
|
2192
|
+
)
|
|
2193
|
+
end
|
|
2194
|
+
unless candidate.nil?
|
|
2195
|
+
run = level.call(candidate)
|
|
2196
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2197
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2198
|
+
)
|
|
2199
|
+
adopt.call(run)
|
|
2200
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2201
|
+
else
|
|
2202
|
+
feedback = FEEDBACK_REJECTED
|
|
2203
|
+
end
|
|
2204
|
+
end
|
|
2205
|
+
when REQUEST_SHORTEN_STRING
|
|
2206
|
+
# One character removed from a string draw.
|
|
2207
|
+
candidate = shorten_string(draws[shorten_cursor], ordinal)
|
|
2208
|
+
unless candidate.nil?
|
|
2209
|
+
trial = draws.dup
|
|
2210
|
+
trial[shorten_cursor] = candidate
|
|
2211
|
+
run = level.call(serialized_draws(trial))
|
|
2212
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2213
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2214
|
+
)
|
|
2215
|
+
adopt.call(run)
|
|
2216
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2217
|
+
else
|
|
2218
|
+
feedback = FEEDBACK_REJECTED
|
|
2219
|
+
end
|
|
2220
|
+
end
|
|
2221
|
+
when REQUEST_LOWER_CHARACTERS
|
|
2222
|
+
# Every copy of one repeated character in a string draw, lowered
|
|
2223
|
+
# together.
|
|
2224
|
+
candidate = lower_characters(draws[character_cursor], ordinal)
|
|
2225
|
+
unless candidate.nil?
|
|
2226
|
+
trial = draws.dup
|
|
2227
|
+
trial[character_cursor] = candidate
|
|
2228
|
+
run = level.call(serialized_draws(trial))
|
|
2229
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2230
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2231
|
+
)
|
|
2232
|
+
adopt.call(run)
|
|
2233
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2234
|
+
else
|
|
2235
|
+
feedback = FEEDBACK_REJECTED
|
|
2236
|
+
end
|
|
2237
|
+
end
|
|
2238
|
+
when REQUEST_RAISE
|
|
2239
|
+
# One draw made *less* simple, kept only when the run that comes back
|
|
2240
|
+
# is smaller: a gate past its threshold, so the draws behind it are
|
|
2241
|
+
# never made. The candidate is larger as written, so the accept rule
|
|
2242
|
+
# judges the executed run alone.
|
|
2243
|
+
current, largest = raise_bounds(draws[raise_cursor])
|
|
2244
|
+
index = current.nil? ? nil : Core.shrink_raise_candidate(current, largest, ordinal)
|
|
2245
|
+
unless index.nil?
|
|
2246
|
+
candidate = draw_at_index(draws[raise_cursor], index)
|
|
2247
|
+
if candidate.equal?(SCHEDULE_MISS)
|
|
2248
|
+
# No legal value there; try the next raise without running.
|
|
2249
|
+
feedback = FEEDBACK_REJECTED
|
|
2250
|
+
else
|
|
2251
|
+
trial = draws.dup
|
|
2252
|
+
trial[raise_cursor] = candidate
|
|
2253
|
+
run = level.call(serialized_draws(trial))
|
|
2254
|
+
if failure?(run.error, origin) && Core.shrink_cost_progress(
|
|
2255
|
+
run.family.__send__(:total_cost), cost, draws_simpler?(run.family.__send__(:draws), family_draws)
|
|
2256
|
+
)
|
|
2257
|
+
adopt.call(run)
|
|
2258
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2259
|
+
else
|
|
2260
|
+
feedback = FEEDBACK_REJECTED
|
|
2261
|
+
end
|
|
2262
|
+
end
|
|
2263
|
+
end
|
|
2264
|
+
when REQUEST_VERIFY
|
|
2265
|
+
run = level.call(serialized_draws(draws))
|
|
2266
|
+
raise FlakyReplay unless failure?(run.error, origin)
|
|
2267
|
+
|
|
2268
|
+
verified = [run.error, run.family]
|
|
2269
|
+
best = run.family
|
|
2270
|
+
best_subject = run.subject
|
|
2271
|
+
feedback = FEEDBACK_ACCEPTED
|
|
2272
|
+
end
|
|
2273
|
+
|
|
2274
|
+
phase, span_cursor, duplicate_cursor, position, pair_cursor,
|
|
2275
|
+
reorder_cursor, trivial_cursor, descendant_cursor, character_cursor,
|
|
2276
|
+
shorten_cursor, paid_cursor, raise_cursor, ordinal, shrinks, stall = [
|
|
2277
|
+
Core.shrink_next_phase(
|
|
2278
|
+
phase, feedback, span_cursor, duplicate_cursor, position, pair_cursor,
|
|
2279
|
+
reorder_cursor, trivial_cursor, descendant_cursor, character_cursor,
|
|
2280
|
+
shorten_cursor, paid_cursor, raise_cursor, shrinks, stall,
|
|
2281
|
+
max_shrinks, max_stall, draws.length, spans.length
|
|
2282
|
+
),
|
|
2283
|
+
Core.shrink_next_span_cursor(phase, feedback, span_cursor),
|
|
2284
|
+
Core.shrink_next_duplicate_cursor(phase, feedback, duplicate_cursor),
|
|
2285
|
+
Core.shrink_next_position(phase, feedback, position),
|
|
2286
|
+
Core.shrink_next_pair_cursor(phase, feedback, pair_cursor),
|
|
2287
|
+
Core.shrink_next_reorder_cursor(phase, feedback, reorder_cursor),
|
|
2288
|
+
Core.shrink_next_trivial_cursor(phase, feedback, trivial_cursor),
|
|
2289
|
+
Core.shrink_next_descendant_cursor(phase, feedback, descendant_cursor),
|
|
2290
|
+
Core.shrink_next_character_cursor(phase, feedback, character_cursor),
|
|
2291
|
+
Core.shrink_next_shorten_cursor(phase, feedback, shorten_cursor),
|
|
2292
|
+
Core.shrink_next_paid_cursor(phase, feedback, paid_cursor),
|
|
2293
|
+
Core.shrink_next_raise_cursor(phase, feedback, raise_cursor),
|
|
2294
|
+
Core.shrink_next_ordinal(phase, feedback, ordinal),
|
|
2295
|
+
Core.shrink_next_shrinks(phase, feedback, shrinks),
|
|
2296
|
+
Core.shrink_next_stall(stall, feedback)
|
|
2297
|
+
]
|
|
2298
|
+
end
|
|
2299
|
+
|
|
2300
|
+
[verified, best, best_subject, accepted, shrinks]
|
|
2301
|
+
end
|
|
2302
|
+
|
|
2303
|
+
# Every clone position in a family, outermost first, as the index path from
|
|
2304
|
+
# the family's own stream down to the clone.
|
|
2305
|
+
#
|
|
2306
|
+
# A path rather than a reference because each candidate re-runs the
|
|
2307
|
+
# property and so builds a whole new family: the case a path names survives
|
|
2308
|
+
# that, a reference into the previous run does not.
|
|
2309
|
+
def clone_paths(draws, prefix = [])
|
|
2310
|
+
paths = []
|
|
2311
|
+
draws.each_with_index do |draw, at|
|
|
2312
|
+
next if draw.child.nil?
|
|
2313
|
+
|
|
2314
|
+
path = prefix + [at]
|
|
2315
|
+
paths << path
|
|
2316
|
+
paths.concat(clone_paths(draw.child.__send__(:draws), path))
|
|
2317
|
+
end
|
|
2318
|
+
paths
|
|
2319
|
+
end
|
|
2320
|
+
|
|
2321
|
+
# Walk a path from a family to the clone stream it names.
|
|
2322
|
+
def case_at(family, path)
|
|
2323
|
+
at = family
|
|
2324
|
+
path.each do |index|
|
|
2325
|
+
return nil if index >= at.__send__(:draws).length || at.__send__(:draws)[index].child.nil?
|
|
2326
|
+
|
|
2327
|
+
at = at.__send__(:draws)[index].child
|
|
2328
|
+
end
|
|
2329
|
+
at
|
|
2330
|
+
end
|
|
2331
|
+
|
|
2332
|
+
# Rebuild stream with the clone named by path carrying inner.
|
|
2333
|
+
#
|
|
2334
|
+
# This is the extract-and-splice the structural core is built around
|
|
2335
|
+
# (ir/structure.zir): the bytes inside a clone entry are already a
|
|
2336
|
+
# well-formed choice stream, so descending is one extraction per level and
|
|
2337
|
+
# the recursion lives here rather than in the IR, which forbids it.
|
|
2338
|
+
def splice_at(stream, path, inner)
|
|
2339
|
+
offset = Core.choice_offset_at(stream, path.first)
|
|
2340
|
+
return nil if offset.nil?
|
|
2341
|
+
|
|
2342
|
+
if path.length > 1
|
|
2343
|
+
nested = Core.clone_inner_stream(stream, offset)
|
|
2344
|
+
return nil if nested.nil?
|
|
2345
|
+
|
|
2346
|
+
rebuilt = splice_at(nested, path[1..], inner)
|
|
2347
|
+
return nil if rebuilt.nil?
|
|
2348
|
+
|
|
2349
|
+
inner = rebuilt
|
|
2350
|
+
end
|
|
2351
|
+
Core.clone_replace_inner(stream, offset, inner)
|
|
2352
|
+
end
|
|
2353
|
+
|
|
2354
|
+
# The level that shrinks the stream inside one clone.
|
|
2355
|
+
#
|
|
2356
|
+
# Every candidate is spliced into the family's stream at the clone's
|
|
2357
|
+
# position and replayed through the whole property, so what is judged is
|
|
2358
|
+
# the family's outcome — the child's choices matter only through the
|
|
2359
|
+
# failure they still produce. The realized child stream is then read back
|
|
2360
|
+
# out of that run.
|
|
2361
|
+
def clone_level(property, family, path)
|
|
2362
|
+
stream = serialized_draws(family.__send__(:draws))
|
|
2363
|
+
lambda do |candidate|
|
|
2364
|
+
spliced = splice_at(stream, path, candidate)
|
|
2365
|
+
next ShrinkRun.new(family, family, nil) if spliced.nil?
|
|
2366
|
+
|
|
2367
|
+
error, test_case = execute(property, replay: spliced)
|
|
2368
|
+
subject = case_at(test_case, path)
|
|
2369
|
+
# The candidate changed the shape of the run: whatever it is, it is not
|
|
2370
|
+
# this clone's stream any more, so there is nothing to adopt.
|
|
2371
|
+
next ShrinkRun.new(test_case, test_case, nil) if subject.nil?
|
|
2372
|
+
|
|
2373
|
+
ShrinkRun.new(test_case, subject, error)
|
|
2374
|
+
end
|
|
2375
|
+
end
|
|
2376
|
+
|
|
2377
|
+
# Run the whole schedule inside each clone of a family.
|
|
2378
|
+
#
|
|
2379
|
+
# The flat passes treat a clone entry as one atomic choice — they can
|
|
2380
|
+
# delete it or move it, never reach the choices inside it — so a failure
|
|
2381
|
+
# that depends on a value drawn from a clone would otherwise be reported
|
|
2382
|
+
# exactly as generation found it. Each clone gets its own run of the same
|
|
2383
|
+
# compiled driver over its own stream, which is upstream's
|
|
2384
|
+
# shrink_clone_streams.
|
|
2385
|
+
def shrink_clone_streams(property, family, origin, max_shrinks, max_stall)
|
|
2386
|
+
improved = false
|
|
2387
|
+
clone_paths(family.__send__(:draws)).each do |path|
|
|
2388
|
+
subject = case_at(family, path)
|
|
2389
|
+
next if subject.nil? || subject.__send__(:draws).empty?
|
|
2390
|
+
|
|
2391
|
+
_, best, _, accepted = shrink_stream(
|
|
2392
|
+
property, clone_level(property, family, path), family, subject, origin,
|
|
2393
|
+
max_shrinks: max_shrinks, max_stall: max_stall
|
|
2394
|
+
)
|
|
2395
|
+
if accepted
|
|
2396
|
+
family = best
|
|
2397
|
+
improved = true
|
|
2398
|
+
end
|
|
2399
|
+
end
|
|
2400
|
+
[family, improved]
|
|
2401
|
+
end
|
|
2402
|
+
|
|
2403
|
+
# Minimize a failing example: the family's own stream first, then the
|
|
2404
|
+
# stream inside each of its clones, to a fixpoint.
|
|
2405
|
+
#
|
|
2406
|
+
# The two alternate because they feed each other — a deletion at the top
|
|
2407
|
+
# can expose a clone worth descending into, and a smaller child stream can
|
|
2408
|
+
# make a parent candidate legal that was not before. Each accepted
|
|
2409
|
+
# candidate is strictly simpler than the one it replaces, so the
|
|
2410
|
+
# alternation terminates.
|
|
2411
|
+
def shrink(property, initial, origin = nil, max_shrinks: 0, max_stall: 0)
|
|
2412
|
+
level = family_level(property)
|
|
2413
|
+
loop do
|
|
2414
|
+
verified, best, _, accepted, shrinks = shrink_stream(
|
|
2415
|
+
property, level, initial, initial, origin,
|
|
2416
|
+
max_shrinks: max_shrinks, max_stall: max_stall
|
|
2417
|
+
)
|
|
2418
|
+
raise "internal error: shrink finished without verifying" if verified.nil?
|
|
2419
|
+
|
|
2420
|
+
family, improved = shrink_clone_streams(
|
|
2421
|
+
property, best, origin, max_shrinks, max_stall
|
|
2422
|
+
)
|
|
2423
|
+
# A sweep that improved the stream goes round again even when no clone
|
|
2424
|
+
# did, unless it stopped at its shrink cap: a later pass's accept can
|
|
2425
|
+
# make an earlier pass's candidate legal, and one sweep never revisits
|
|
2426
|
+
# it. A state machine needs this most, because a continue decision
|
|
2427
|
+
# misread after a deletion stops the run, so lowering a rule is often
|
|
2428
|
+
# what lets a whole step go.
|
|
2429
|
+
again = Core.shrink_sweep_again(accepted, shrinks, max_shrinks)
|
|
2430
|
+
return verified unless improved || again
|
|
2431
|
+
|
|
2432
|
+
initial = family
|
|
2433
|
+
end
|
|
2434
|
+
end
|
|
2435
|
+
end
|
|
2436
|
+
|
|
2437
|
+
# Run a property, shrink its first failure, and raise it.
|
|
2438
|
+
#
|
|
2439
|
+
# Configuration comes from the active profile (see Zizek.register_profile),
|
|
2440
|
+
# overridden by any keywords passed here, or from an explicit `settings:`
|
|
2441
|
+
# object. Raises Zizek::Failure for a counterexample, and FailedCoverage,
|
|
2442
|
+
# TooManyRejections, or FailedHealthCheck for a run that never got one.
|
|
2443
|
+
# Use Zizek.run for the same search reported as a value instead.
|
|
2444
|
+
def self.check(property = nil, settings: nil, reproduce: nil, **overrides, &block)
|
|
2445
|
+
Shell.explore(property || block, Shell.resolve_settings(settings, overrides), reproduce)
|
|
2446
|
+
nil
|
|
2447
|
+
end
|
|
2448
|
+
|
|
2449
|
+
# The outcome of a run, as a value.
|
|
2450
|
+
#
|
|
2451
|
+
# `check` raises because a test that quietly ignored a counterexample would
|
|
2452
|
+
# be worse than no test. `run` is for the code that wants the verdict
|
|
2453
|
+
# instead: a harness collecting blobs, a script comparing shrink quality, a
|
|
2454
|
+
# property about a property.
|
|
2455
|
+
Result = Struct.new(:outcome, :error, :examples_run, :seed, keyword_init: true) do
|
|
2456
|
+
def ok?
|
|
2457
|
+
outcome == :ok
|
|
2458
|
+
end
|
|
2459
|
+
|
|
2460
|
+
# A counterexample was found. False for a run that failed some other way
|
|
2461
|
+
# (coverage, rejections, a health check), which `ok?` also reports.
|
|
2462
|
+
def failure?
|
|
2463
|
+
outcome == :failure
|
|
2464
|
+
end
|
|
2465
|
+
|
|
2466
|
+
def blob
|
|
2467
|
+
error.blob if error.respond_to?(:blob)
|
|
2468
|
+
end
|
|
2469
|
+
|
|
2470
|
+
def values
|
|
2471
|
+
error.respond_to?(:values) ? error.values : []
|
|
2472
|
+
end
|
|
2473
|
+
|
|
2474
|
+
def notes
|
|
2475
|
+
error.respond_to?(:notes) ? error.notes : []
|
|
2476
|
+
end
|
|
2477
|
+
|
|
2478
|
+
def costs
|
|
2479
|
+
error.respond_to?(:costs) ? error.costs : {}
|
|
2480
|
+
end
|
|
2481
|
+
|
|
2482
|
+
def message
|
|
2483
|
+
error&.message
|
|
2484
|
+
end
|
|
2485
|
+
|
|
2486
|
+
# Raise whatever this run found, so a `run` call site can escalate without
|
|
2487
|
+
# re-deriving which exception class the outcome corresponds to.
|
|
2488
|
+
def raise!
|
|
2489
|
+
raise error unless ok?
|
|
2490
|
+
|
|
2491
|
+
nil
|
|
2492
|
+
end
|
|
2493
|
+
end
|
|
2494
|
+
|
|
2495
|
+
# Run a property and report the outcome instead of raising it.
|
|
2496
|
+
#
|
|
2497
|
+
# result = Zizek.run(property, examples: 500)
|
|
2498
|
+
# puts result.blob unless result.ok?
|
|
2499
|
+
#
|
|
2500
|
+
# An invalid replay blob or a bad argument still raises: those are mistakes
|
|
2501
|
+
# in the call, not outcomes of the search.
|
|
2502
|
+
def self.run(property = nil, settings: nil, reproduce: nil, **overrides, &block)
|
|
2503
|
+
summary = Shell.explore(property || block, Shell.resolve_settings(settings, overrides), reproduce)
|
|
2504
|
+
Result.new(outcome: :ok, examples_run: summary[:examples_run], seed: summary[:seed])
|
|
2505
|
+
rescue Failure => error
|
|
2506
|
+
Result.new(outcome: :failure, error: error, examples_run: error.examples_run, seed: error.seed)
|
|
2507
|
+
rescue Flaky => error
|
|
2508
|
+
Result.new(outcome: :flaky, error: error, examples_run: error.examples_run, seed: error.seed)
|
|
2509
|
+
rescue FailedCoverage => error
|
|
2510
|
+
Result.new(outcome: :coverage, error: error, examples_run: error.valid)
|
|
2511
|
+
rescue TooManyRejections => error
|
|
2512
|
+
Result.new(outcome: :exhausted, error: error)
|
|
2513
|
+
rescue FailedHealthCheck => error
|
|
2514
|
+
Result.new(outcome: :unhealthy, error: error)
|
|
2515
|
+
end
|
|
2516
|
+
|
|
2517
|
+
module Shell
|
|
2518
|
+
module_function
|
|
2519
|
+
|
|
2520
|
+
# Merge an explicit settings object, per-call overrides, and the active
|
|
2521
|
+
# profile into the one Settings the run uses.
|
|
2522
|
+
def resolve_settings(settings, overrides)
|
|
2523
|
+
base = settings || Zizek.current_settings
|
|
2524
|
+
unless base.is_a?(Settings)
|
|
2525
|
+
raise ArgumentError, "settings: must be a Zizek::Settings, got #{base.class}"
|
|
2526
|
+
end
|
|
2527
|
+
|
|
2528
|
+
base.with(**overrides)
|
|
2529
|
+
end
|
|
2530
|
+
|
|
2531
|
+
# Explore, shrink, and adjudicate. Raises on every outcome that is not a
|
|
2532
|
+
# clean pass; returns what a clean pass is worth reporting.
|
|
2533
|
+
def explore(property, config, reproduce)
|
|
2534
|
+
raise ArgumentError, "a property callable or block is required" if property.nil?
|
|
2535
|
+
|
|
2536
|
+
examples = config.examples
|
|
2537
|
+
max_rejections = config.max_rejections
|
|
2538
|
+
max_shrinks = config.max_shrinks
|
|
2539
|
+
max_stall = config.max_stall
|
|
2540
|
+
suppress_filter_too_much = config.suppress_filter_too_much
|
|
2541
|
+
suppress_data_too_large = config.suppress_data_too_large
|
|
2542
|
+
suppress_large_base_example = config.suppress_large_base_example
|
|
2543
|
+
suppress_too_slow = config.suppress_too_slow
|
|
2544
|
+
required_coverage = config.must_cover
|
|
2545
|
+
|
|
2546
|
+
unless reproduce.nil?
|
|
2547
|
+
raw = Core.raw_failure_decode(reproduce.b)
|
|
2548
|
+
raise InvalidReplay, "invalid or unsupported reproduce blob" if raw.nil?
|
|
2549
|
+
|
|
2550
|
+
error, replay_case = Shell.execute(property, replay: raw)
|
|
2551
|
+
raise InvalidReplay, "reproduced example did not fail" if error.nil?
|
|
2552
|
+
raise InvalidReplay, "reproduced example was rejected by an assumption" if error.is_a?(AssumptionRejected)
|
|
2553
|
+
raise error if error.is_a?(ReplayOverrun) || error.is_a?(InvalidReplay)
|
|
2554
|
+
|
|
2555
|
+
raise Failure.new(
|
|
2556
|
+
blob: reproduce, property_cause: error, examples_run: 1, rejections: 0,
|
|
2557
|
+
notes: replay_case.__send__(:notes), values: replay_case.__send__(:drawn), puns: replay_case.__send__(:puns),
|
|
2558
|
+
key_misses: replay_case.__send__(:key_misses), costs: replay_case.__send__(:costs)
|
|
2559
|
+
)
|
|
2560
|
+
end
|
|
2561
|
+
|
|
2562
|
+
seed = config.__send__(:effective_seed, property)
|
|
2563
|
+
database = Database.__send__(:resolve, config.database)
|
|
2564
|
+
database_key = config.database_key
|
|
2565
|
+
# Deliberately local to Ruby: where a property was written says nothing
|
|
2566
|
+
# to the Python or Go suite testing the same behaviour, and a key that
|
|
2567
|
+
# pretended otherwise would file a counterexample where the other
|
|
2568
|
+
# language will never look for it. Set `database_key:` to share one.
|
|
2569
|
+
database_key ||= Settings.__send__(:property_identity, property) unless database.nil?
|
|
2570
|
+
coverage = []
|
|
2571
|
+
coverage_index = {}
|
|
2572
|
+
required_coverage.each do |required|
|
|
2573
|
+
permille = (required.percent * 10).to_i
|
|
2574
|
+
if coverage_index.key?(required.label)
|
|
2575
|
+
at = coverage_index[required.label]
|
|
2576
|
+
coverage[at].permille = [coverage[at].permille, permille].max
|
|
2577
|
+
next
|
|
2578
|
+
end
|
|
2579
|
+
coverage_index[required.label] = coverage.length
|
|
2580
|
+
coverage << Shell::CoverageCounter.new(required.label, permille, 0)
|
|
2581
|
+
end
|
|
2582
|
+
rng = Random.new(seed)
|
|
2583
|
+
valid = 0
|
|
2584
|
+
attempts = 0
|
|
2585
|
+
# Database replays are attempts but not generation. The schedule is told
|
|
2586
|
+
# generation's own count, so a stale entry cannot take the all-simplest
|
|
2587
|
+
# probe's place as the first request.
|
|
2588
|
+
replays = 0
|
|
2589
|
+
rejections = 0
|
|
2590
|
+
overruns = 0
|
|
2591
|
+
generation_us = 0
|
|
2592
|
+
pending_mutations = []
|
|
2593
|
+
# Consecutive executed mutation requests; the schedule caps it at one
|
|
2594
|
+
# brood so fresh random generation keeps its share of the run.
|
|
2595
|
+
mutation_streak = 0
|
|
2596
|
+
choice_tree = Shell::ChoiceTree.new
|
|
2597
|
+
# Per-label best score and the stream that produced it, for the targeting
|
|
2598
|
+
# hill climber. The schedule, budgets, record and accept rules, stepping,
|
|
2599
|
+
# and the step-size search are all compiled (ir/targeting.zir); this loop
|
|
2600
|
+
# holds the maps and runs the trials.
|
|
2601
|
+
targeting_best = {}
|
|
2602
|
+
targeting_step = Core.targeting_schedule_step(examples)
|
|
2603
|
+
targeting_next_at = targeting_step
|
|
2604
|
+
targeting_trial_budget = Core.targeting_trial_budget(examples)
|
|
2605
|
+
|
|
2606
|
+
# Fold a valid example's targeting observations into the per-label bests.
|
|
2607
|
+
record_targets = lambda do |test_case|
|
|
2608
|
+
test_case.__send__(:targets).each do |label, score|
|
|
2609
|
+
entry = targeting_best[label]
|
|
2610
|
+
if Core.targeting_record(!entry.nil?, entry.nil? ? 0.0 : entry[0], score)
|
|
2611
|
+
targeting_best[label] = [score, test_case.__send__(:serialized)]
|
|
2612
|
+
end
|
|
2613
|
+
end
|
|
2614
|
+
end
|
|
2615
|
+
|
|
2616
|
+
# Shrink a failing case, store what it shrank to, and build the failure.
|
|
2617
|
+
# A replay during the search that stops failing is reported as Flaky,
|
|
2618
|
+
# carrying the failure as found rather than a shrunk one that cannot be
|
|
2619
|
+
# trusted; nothing is stored or collected for it.
|
|
2620
|
+
counterexample = lambda do |test_case, error, replaced|
|
|
2621
|
+
begin
|
|
2622
|
+
shrunk_error, shrunk_case = Shell.shrink(
|
|
2623
|
+
property, test_case, Shell.interesting_origin(error),
|
|
2624
|
+
max_shrinks: max_shrinks, max_stall: max_stall
|
|
2625
|
+
)
|
|
2626
|
+
rescue FlakyReplay
|
|
2627
|
+
next Flaky.new(
|
|
2628
|
+
blob: Core.raw_failure_encode(test_case.__send__(:serialized)), property_cause: error,
|
|
2629
|
+
examples_run: attempts, rejections: rejections, seed: seed
|
|
2630
|
+
)
|
|
2631
|
+
end
|
|
2632
|
+
stream = shrunk_case.__send__(:serialized)
|
|
2633
|
+
unless database.nil?
|
|
2634
|
+
database.save(database_key, stream)
|
|
2635
|
+
# The entry that reproduced is superseded by whatever it shrank to, so
|
|
2636
|
+
# it goes; an entry that shrank to itself is already stored.
|
|
2637
|
+
database.delete(database_key, replaced) if !replaced.nil? && replaced != stream
|
|
2638
|
+
end
|
|
2639
|
+
Failure.new(
|
|
2640
|
+
blob: Core.raw_failure_encode(stream), property_cause: shrunk_error,
|
|
2641
|
+
examples_run: attempts, rejections: rejections,
|
|
2642
|
+
notes: shrunk_case.__send__(:notes), values: shrunk_case.__send__(:drawn), seed: seed,
|
|
2643
|
+
puns: shrunk_case.__send__(:puns), key_misses: shrunk_case.__send__(:key_misses),
|
|
2644
|
+
costs: shrunk_case.__send__(:costs)
|
|
2645
|
+
)
|
|
2646
|
+
end
|
|
2647
|
+
|
|
2648
|
+
# One targeting trial: replay a stepped stream with fresh-RNG extension —
|
|
2649
|
+
# the execution mode mutation probes already use — counting it as an
|
|
2650
|
+
# ordinary attempt. Answers nil when the firing's budget is spent,
|
|
2651
|
+
# otherwise [error, test_case, outcome].
|
|
2652
|
+
targeting_trial = lambda do |stream|
|
|
2653
|
+
if valid >= examples || attempts >= targeting_trial_budget
|
|
2654
|
+
nil
|
|
2655
|
+
else
|
|
2656
|
+
attempts += 1
|
|
2657
|
+
error, trial_case = Shell.execute(property, rng: rng, replay: stream)
|
|
2658
|
+
choice_tree.insert(trial_case.__send__(:serialized))
|
|
2659
|
+
if error.nil?
|
|
2660
|
+
valid += 1
|
|
2661
|
+
record_targets.call(trial_case)
|
|
2662
|
+
end
|
|
2663
|
+
[error, trial_case, Shell.classify(error)]
|
|
2664
|
+
end
|
|
2665
|
+
end
|
|
2666
|
+
|
|
2667
|
+
# Hill-climb one label's best-known stream: walk its realized draws from
|
|
2668
|
+
# the end backwards, stepping each climbable draw through the compiled
|
|
2669
|
+
# step-size search, in the +k direction and then -k. Answers
|
|
2670
|
+
# [improvements, interesting], where interesting is a failing trial the
|
|
2671
|
+
# ordinary counterexample path takes over. Mirrors
|
|
2672
|
+
# hegel-c/src/native/targeting.rs::hill_climb.
|
|
2673
|
+
targeting_climb = lambda do |label, allowance|
|
|
2674
|
+
trial = targeting_trial.call(targeting_best[label][1])
|
|
2675
|
+
return [0, nil] if trial.nil?
|
|
2676
|
+
|
|
2677
|
+
error, current_case, outcome = trial
|
|
2678
|
+
return [0, [error, current_case]] if outcome == Shell::OUTCOME_INTERESTING
|
|
2679
|
+
return [0, nil] unless error.nil?
|
|
2680
|
+
|
|
2681
|
+
current_score = current_case.__send__(:targets).fetch(label, -Float::INFINITY)
|
|
2682
|
+
improvements = 0
|
|
2683
|
+
interesting = nil
|
|
2684
|
+
examined = {}
|
|
2685
|
+
at = current_case.__send__(:draws).length - 1
|
|
2686
|
+
previous_length = current_case.__send__(:draws).length
|
|
2687
|
+
while at >= 0 && improvements <= allowance && interesting.nil?
|
|
2688
|
+
if current_case.__send__(:draws).length != previous_length
|
|
2689
|
+
at = current_case.__send__(:draws).length - 1
|
|
2690
|
+
previous_length = current_case.__send__(:draws).length
|
|
2691
|
+
next
|
|
2692
|
+
end
|
|
2693
|
+
if examined[at]
|
|
2694
|
+
at -= 1
|
|
2695
|
+
next
|
|
2696
|
+
end
|
|
2697
|
+
examined[at] = true
|
|
2698
|
+
draw = current_case.__send__(:draws)[at]
|
|
2699
|
+
if Shell.climbable_draw?(draw)
|
|
2700
|
+
length_before = current_case.__send__(:draws).length
|
|
2701
|
+
[1, -1].each do |direction|
|
|
2702
|
+
break unless interesting.nil?
|
|
2703
|
+
# The -k direction only makes sense while the draw survived the
|
|
2704
|
+
# +k climb at the same index with the stream length unchanged.
|
|
2705
|
+
if direction == -1 &&
|
|
2706
|
+
(at >= current_case.__send__(:draws).length || current_case.__send__(:draws).length != length_before)
|
|
2707
|
+
break
|
|
2708
|
+
end
|
|
2709
|
+
|
|
2710
|
+
mode = 0
|
|
2711
|
+
a = 1
|
|
2712
|
+
b = 0
|
|
2713
|
+
loop do
|
|
2714
|
+
# A committed improvement may have shortened the stream below
|
|
2715
|
+
# this index; the outer walk restarts from the new end.
|
|
2716
|
+
break if at >= current_case.__send__(:draws).length
|
|
2717
|
+
|
|
2718
|
+
probe = Core.find_integer_probe(mode, a, b)
|
|
2719
|
+
break if probe.nil?
|
|
2720
|
+
|
|
2721
|
+
committed = false
|
|
2722
|
+
if probe <= Core.targeting_step_cap
|
|
2723
|
+
stepped = Shell.step_draw(current_case.__send__(:draws)[at], direction * probe)
|
|
2724
|
+
unless stepped.nil?
|
|
2725
|
+
draws = current_case.__send__(:draws).dup
|
|
2726
|
+
draws[at] = stepped
|
|
2727
|
+
trial = targeting_trial.call(Shell.serialized_draws(draws))
|
|
2728
|
+
break if trial.nil?
|
|
2729
|
+
|
|
2730
|
+
trial_error, trial_case, trial_outcome = trial
|
|
2731
|
+
if trial_outcome == Shell::OUTCOME_INTERESTING
|
|
2732
|
+
interesting = [trial_error, trial_case]
|
|
2733
|
+
break
|
|
2734
|
+
end
|
|
2735
|
+
if trial_error.nil?
|
|
2736
|
+
trial_score = trial_case.__send__(:targets).fetch(label, -Float::INFINITY)
|
|
2737
|
+
accept = Core.targeting_accept(
|
|
2738
|
+
trial_score, current_score,
|
|
2739
|
+
Shell.flat_draw_count(trial_case.__send__(:draws)),
|
|
2740
|
+
Shell.flat_draw_count(current_case.__send__(:draws))
|
|
2741
|
+
)
|
|
2742
|
+
unless accept.zero?
|
|
2743
|
+
current_case = trial_case
|
|
2744
|
+
current_score = trial_score
|
|
2745
|
+
improvements += 1 if accept == 2
|
|
2746
|
+
committed = true
|
|
2747
|
+
end
|
|
2748
|
+
end
|
|
2749
|
+
end
|
|
2750
|
+
end
|
|
2751
|
+
next_mode = Core.find_integer_next_mode(mode, a, b, committed)
|
|
2752
|
+
next_a = Core.find_integer_next_a(mode, a, b, committed)
|
|
2753
|
+
next_b = Core.find_integer_next_b(mode, a, b, committed)
|
|
2754
|
+
mode = next_mode
|
|
2755
|
+
a = next_a
|
|
2756
|
+
b = next_b
|
|
2757
|
+
end
|
|
2758
|
+
end
|
|
2759
|
+
end
|
|
2760
|
+
at -= 1
|
|
2761
|
+
end
|
|
2762
|
+
[improvements, interesting]
|
|
2763
|
+
end
|
|
2764
|
+
|
|
2765
|
+
# Climb every label until a full round improves nothing or the budget is
|
|
2766
|
+
# spent. Mirrors targeting.rs::optimise_targets, with the improvement
|
|
2767
|
+
# allowance doubling each round.
|
|
2768
|
+
optimise_targets = lambda do
|
|
2769
|
+
round = 0
|
|
2770
|
+
loop do
|
|
2771
|
+
calls_before = attempts
|
|
2772
|
+
any_improvements = false
|
|
2773
|
+
allowance = Core.targeting_improvement_allowance(round)
|
|
2774
|
+
targeting_best.keys.sort.each do |label|
|
|
2775
|
+
improvements, interesting = targeting_climb.call(label, allowance)
|
|
2776
|
+
return interesting unless interesting.nil?
|
|
2777
|
+
|
|
2778
|
+
any_improvements = true if improvements.positive?
|
|
2779
|
+
end
|
|
2780
|
+
round += 1
|
|
2781
|
+
return nil if !any_improvements || attempts == calls_before
|
|
2782
|
+
end
|
|
2783
|
+
end
|
|
2784
|
+
|
|
2785
|
+
# Reuse before generate: replay what earlier runs stored under this name,
|
|
2786
|
+
# simplest first. An entry that still fails is the answer; one that does
|
|
2787
|
+
# not is a bug that was fixed, and collecting it now keeps every future run
|
|
2788
|
+
# from paying for it. Replays are counted as attempts but not as valid
|
|
2789
|
+
# examples, so a stored corpus never eats the exploration budget, and they
|
|
2790
|
+
# feed neither the coverage verdict nor the health checks — both are
|
|
2791
|
+
# statements about what this run explored.
|
|
2792
|
+
unless database.nil?
|
|
2793
|
+
database.fetch(database_key).sort_by { |entry| Database.__send__(:shortlex, entry) }.each do |entry|
|
|
2794
|
+
unless Core.choice_stream_validate(entry)
|
|
2795
|
+
database.delete(database_key, entry)
|
|
2796
|
+
next
|
|
2797
|
+
end
|
|
2798
|
+
attempts += 1
|
|
2799
|
+
replays += 1
|
|
2800
|
+
error, test_case = Shell.execute(property, replay: entry)
|
|
2801
|
+
if Shell.classify(error) != Shell::OUTCOME_INTERESTING
|
|
2802
|
+
database.delete(database_key, entry)
|
|
2803
|
+
next
|
|
2804
|
+
end
|
|
2805
|
+
raise counterexample.call(test_case, error, entry)
|
|
2806
|
+
end
|
|
2807
|
+
end
|
|
2808
|
+
|
|
2809
|
+
while Core.generation_should_continue(valid, examples)
|
|
2810
|
+
request = Core.generation_request_kind(attempts - replays, pending_mutations.length, mutation_streak)
|
|
2811
|
+
simplest_probe = request == 1
|
|
2812
|
+
mutation_probe = request == 2
|
|
2813
|
+
mutation = mutation_probe ? pending_mutations.shift : nil
|
|
2814
|
+
novelty = Core.generation_novelty_action(request, mutation_probe && choice_tree.include?(mutation))
|
|
2815
|
+
next if novelty.zero?
|
|
2816
|
+
choice_tree.insert(mutation) if novelty == 2
|
|
2817
|
+
attempts += 1
|
|
2818
|
+
mutation_streak = mutation_probe ? mutation_streak + 1 : 0
|
|
2819
|
+
error, test_case = Shell.execute(
|
|
2820
|
+
property,
|
|
2821
|
+
rng: simplest_probe ? nil : rng,
|
|
2822
|
+
replay: mutation,
|
|
2823
|
+
simplest: simplest_probe
|
|
2824
|
+
)
|
|
2825
|
+
choice_tree.insert(test_case.__send__(:serialized))
|
|
2826
|
+
generation_us = [((1 << 64) - 1), generation_us + test_case.__send__(:generation_us)].min
|
|
2827
|
+
outcome = Shell.classify(error)
|
|
2828
|
+
if Core.health_large_base_example(
|
|
2829
|
+
error.is_a?(ChoiceOverrun),
|
|
2830
|
+
outcome,
|
|
2831
|
+
test_case.__send__(:draws).length,
|
|
2832
|
+
suppress_large_base_example
|
|
2833
|
+
)
|
|
2834
|
+
raise FailedHealthCheck.new(
|
|
2835
|
+
:large_base_example,
|
|
2836
|
+
"large_base_example: the simplest natural input is too large"
|
|
2837
|
+
)
|
|
2838
|
+
end
|
|
2839
|
+
health_valid = valid + (error.nil? ? 1 : 0)
|
|
2840
|
+
if outcome != Shell::OUTCOME_INTERESTING &&
|
|
2841
|
+
Core.health_too_slow(generation_us, health_valid, suppress_too_slow)
|
|
2842
|
+
raise FailedHealthCheck.new(
|
|
2843
|
+
:too_slow,
|
|
2844
|
+
"too_slow: input generation exceeded one cumulative second " \
|
|
2845
|
+
"while only #{health_valid} valid inputs were generated"
|
|
2846
|
+
)
|
|
2847
|
+
end
|
|
2848
|
+
if error.nil?
|
|
2849
|
+
valid += 1
|
|
2850
|
+
# An example that reached a label the run has barely covered is on the
|
|
2851
|
+
# frontier, so its mutation proposals jump the queue ahead of ones bred
|
|
2852
|
+
# from the common path, and may be bred at all when the base rule
|
|
2853
|
+
# declines.
|
|
2854
|
+
held_rare = false
|
|
2855
|
+
test_case.__send__(:coverage_order).each do |label|
|
|
2856
|
+
unless coverage_index.key?(label)
|
|
2857
|
+
coverage_index[label] = coverage.length
|
|
2858
|
+
coverage << Shell::CoverageCounter.new(label, 0, 0)
|
|
2859
|
+
end
|
|
2860
|
+
counter = coverage[coverage_index[label]]
|
|
2861
|
+
counter.permille = [counter.permille, test_case.__send__(:coverage_permille)[label]].max
|
|
2862
|
+
next unless test_case.__send__(:coverage_held)[label]
|
|
2863
|
+
|
|
2864
|
+
counter.observed += 1
|
|
2865
|
+
held_rare = true if Core.generation_label_rare(counter.observed, valid, counter.permille)
|
|
2866
|
+
end
|
|
2867
|
+
admission = Core.generation_mutation_admission(
|
|
2868
|
+
request, valid, pending_mutations.length, held_rare
|
|
2869
|
+
)
|
|
2870
|
+
unless admission.zero?
|
|
2871
|
+
proposals = Shell.span_mutation_candidates(test_case.__send__(:draws), test_case.__send__(:spans), rng)
|
|
2872
|
+
if admission == 2
|
|
2873
|
+
pending_mutations.unshift(*proposals)
|
|
2874
|
+
else
|
|
2875
|
+
pending_mutations.concat(proposals)
|
|
2876
|
+
end
|
|
2877
|
+
end
|
|
2878
|
+
# Targeting: fold this example's observations in, and fire the hill
|
|
2879
|
+
# climber when the schedule says so. A failing trial is a
|
|
2880
|
+
# counterexample like any other.
|
|
2881
|
+
record_targets.call(test_case)
|
|
2882
|
+
if !targeting_best.empty? && Core.targeting_should_fire(valid, targeting_next_at)
|
|
2883
|
+
targeting_next_at = Core.targeting_next_threshold(targeting_next_at, targeting_step)
|
|
2884
|
+
found = optimise_targets.call
|
|
2885
|
+
raise counterexample.call(found[1], found[0], nil) unless found.nil?
|
|
2886
|
+
end
|
|
2887
|
+
next
|
|
2888
|
+
end
|
|
2889
|
+
if error.is_a?(AssumptionRejected)
|
|
2890
|
+
rejections += 1
|
|
2891
|
+
rejection_limit = Core.generation_rejection_limit(max_rejections, valid)
|
|
2892
|
+
if rejections + overruns > rejection_limit
|
|
2893
|
+
raise TooManyRejections.new(rejections + overruns, rejection_limit)
|
|
2894
|
+
end
|
|
2895
|
+
if Core.health_filter_too_much(rejections, valid, suppress_filter_too_much)
|
|
2896
|
+
raise FailedHealthCheck.new(
|
|
2897
|
+
:filter_too_much,
|
|
2898
|
+
"filter_too_much: #{rejections} inputs were rejected by assumptions " \
|
|
2899
|
+
"while only #{valid} valid inputs were generated"
|
|
2900
|
+
)
|
|
2901
|
+
end
|
|
2902
|
+
next
|
|
2903
|
+
end
|
|
2904
|
+
if error.is_a?(ChoiceOverrun)
|
|
2905
|
+
overruns += 1
|
|
2906
|
+
rejection_limit = Core.generation_rejection_limit(max_rejections, valid)
|
|
2907
|
+
if rejections + overruns > rejection_limit
|
|
2908
|
+
raise TooManyRejections.new(rejections + overruns, rejection_limit)
|
|
2909
|
+
end
|
|
2910
|
+
if Core.health_data_too_large(overruns, valid, suppress_data_too_large)
|
|
2911
|
+
raise FailedHealthCheck.new(
|
|
2912
|
+
:data_too_large,
|
|
2913
|
+
"data_too_large: #{overruns} inputs exceeded the choice budget " \
|
|
2914
|
+
"while only #{valid} valid inputs were generated"
|
|
2915
|
+
)
|
|
2916
|
+
end
|
|
2917
|
+
next
|
|
2918
|
+
end
|
|
2919
|
+
raise error if error.is_a?(InvalidReplay)
|
|
2920
|
+
|
|
2921
|
+
raise counterexample.call(test_case, error, nil)
|
|
2922
|
+
end
|
|
2923
|
+
# The coverage verdict runs only on a run that completed fully valid: a
|
|
2924
|
+
# counterexample or an aborted run reports that instead, and a verdict
|
|
2925
|
+
# about a truncated run would be noise. The accept rule is compiled
|
|
2926
|
+
# (ir/lifecycle.zir @coverage_failed) so every shell agrees on it.
|
|
2927
|
+
coverage.each do |counter|
|
|
2928
|
+
next unless Core.coverage_failed(counter.observed, valid, counter.permille)
|
|
2929
|
+
|
|
2930
|
+
raise FailedCoverage.new(counter.label, counter.observed, valid, counter.permille / 10.0)
|
|
2931
|
+
end
|
|
2932
|
+
{ examples_run: attempts, valid: valid, rejections: rejections, seed: seed }
|
|
2933
|
+
end
|
|
2934
|
+
end
|
|
2935
|
+
|
|
2936
|
+
private_class_method :key_encode
|
|
2937
|
+
private_constant :Core, :Shell, :Draw, :ChoiceSpan, :UNICODE
|
|
2938
|
+
end
|