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.
@@ -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