hegeltest 0.3.4-aarch64-linux → 0.4.1-aarch64-linux

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +221 -0
  3. data/README.ja.md +408 -0
  4. data/README.md +208 -102
  5. data/Rakefile +6 -3
  6. data/docs/README.md +11 -0
  7. data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
  8. data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
  9. data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
  10. data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
  11. data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
  12. data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
  13. data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
  14. data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
  15. data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
  16. data/docs/adr/0036-follow-rubys-unicode-version.md +50 -0
  17. data/docs/adr/0037-send-a-maximum-at-the-engine-default-sentinel-as-no-bound.md +48 -0
  18. data/docs/architecture.md +16 -1
  19. data/lib/hegel/errors.rb +24 -0
  20. data/lib/hegel/generator.rb +52 -25
  21. data/lib/hegel/generators.rb +715 -45
  22. data/lib/hegel/grammar_cache.rb +36 -0
  23. data/lib/hegel/lib_hegel/real.rb +333 -19
  24. data/lib/hegel/lib_hegel.rb +63 -6
  25. data/lib/hegel/libhegel/libhegel-linux-arm64.so +0 -0
  26. data/lib/hegel/libhegel_version.rb +1 -1
  27. data/lib/hegel/report.rb +13 -10
  28. data/lib/hegel/runner.rb +61 -27
  29. data/lib/hegel/settings.rb +92 -16
  30. data/lib/hegel/state_machine.rb +17 -4
  31. data/lib/hegel/stateful/pool.rb +52 -0
  32. data/lib/hegel/stateful.rb +2 -2
  33. data/lib/hegel/syntax/methods.rb +99 -15
  34. data/lib/hegel/test_case.rb +259 -37
  35. data/lib/hegel/test_location.rb +49 -0
  36. data/lib/hegel/version.rb +1 -1
  37. data/lib/hegel.rb +21 -5
  38. data/sig/hegel.rbs +300 -42
  39. data/skills/hegel-ruby/references/ruby/reference.md +463 -41
  40. metadata +15 -1
@@ -27,26 +27,34 @@ module Hegel
27
27
  # A double in [min_value, max_value]. allow_nan and allow_infinity
28
28
  # default to what the bounds imply: NaN when the caller passed
29
29
  # neither bound, an infinity when the caller left either side open.
30
+ # allow_subnormal defaults to true whenever the bounds admit a
31
+ # subnormal.
30
32
  def floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false,
31
- exclude_max: false)
33
+ exclude_max: false, allow_subnormal: nil)
32
34
  Generators::FloatGenerator.new(
33
35
  min_value: min_value, max_value: max_value, allow_nan: allow_nan, allow_infinity: allow_infinity,
34
- exclude_min: exclude_min, exclude_max: exclude_max
36
+ exclude_min: exclude_min, exclude_max: exclude_max, allow_subnormal: allow_subnormal
35
37
  )
36
38
  end
37
39
 
38
- # A Unicode string of [min_size, max_size] characters.
39
- def text(min_size: 0, max_size: nil, codec: nil, min_codepoint: nil, max_codepoint: nil)
40
+ # A Unicode string of [min_size, max_size] characters; max_size
41
+ # defaults to 100, or min_size + 100 when min_size exceeds 100.
42
+ # +alphabet+ is a String of the permitted characters; it cannot be
43
+ # combined with the options after it.
44
+ def text(min_size: 0, max_size: nil, alphabet: nil, codec: nil, min_codepoint: nil, max_codepoint: nil,
45
+ categories: nil, exclude_categories: nil, include_characters: nil, exclude_characters: nil)
40
46
  Generators::TextGenerator.new(
41
- min_size: min_size, max_size: max_size, codec: codec,
42
- min_codepoint: min_codepoint, max_codepoint: max_codepoint
47
+ min_size: min_size, max_size: max_size, alphabet: alphabet, codec: codec,
48
+ min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
49
+ exclude_categories: exclude_categories, include_characters: include_characters,
50
+ exclude_characters: exclude_characters
43
51
  )
44
52
  end
45
53
 
46
54
  # An Array of values from +elements+, with [min_size, max_size]
47
- # entries.
48
- def arrays(elements, min_size: 0, max_size: nil)
49
- Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size)
55
+ # entries. With unique: true, no two entries are equal.
56
+ def arrays(elements, min_size: 0, max_size: nil, unique: false)
57
+ Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size, unique: unique)
50
58
  end
51
59
 
52
60
  # Always +value+, drawing nothing.
@@ -59,6 +67,29 @@ module Hegel
59
67
  Generators::SampledFromGenerator.new(collection)
60
68
  end
61
69
 
70
+ # An Array holding every element of +elements+ (anything #sampled_from
71
+ # accepts) in an order the engine picks, shrinking towards the
72
+ # original one.
73
+ def permutations(elements)
74
+ Generators::PermutationsGenerator.new(elements)
75
+ end
76
+
77
+ # An Array holding [min_size, max_size] of the elements of +elements+,
78
+ # in their original order. Duplicate elements count as distinct.
79
+ def subsequences(elements, min_size: 0, max_size: nil)
80
+ Generators::SubsequencesGenerator.new(elements, min_size: min_size, max_size: max_size)
81
+ end
82
+
83
+ # An Array of [min_size, max_size] elements of +elements+. With
84
+ # +replacement+ (the default) each is drawn independently and the
85
+ # size is unbounded unless +max_size+ bounds it. Without it, each
86
+ # element is used at most once and the size is capped at the number
87
+ # of elements; hegel-rust names the two modes with_replacement and
88
+ # without_replacement.
89
+ def samples(elements, min_size: 0, max_size: nil, replacement: true)
90
+ Generators::SamplesGenerator.new(elements, min_size: min_size, max_size: max_size, replacement: replacement)
91
+ end
92
+
62
93
  # A value drawn from one of +generators+, picked at random.
63
94
  def one_of(*generators)
64
95
  Generators::OneOfGenerator.new(generators)
@@ -88,20 +119,41 @@ module Hegel
88
119
 
89
120
  # A String of exactly one character, sharing #text's own alphabet
90
121
  # options.
91
- def characters(codec: nil, min_codepoint: nil, max_codepoint: nil)
92
- Generators::CharactersGenerator.new(codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint)
122
+ def characters(codec: nil, min_codepoint: nil, max_codepoint: nil, categories: nil, exclude_categories: nil,
123
+ include_characters: nil, exclude_characters: nil)
124
+ Generators::CharactersGenerator.new(
125
+ codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
126
+ exclude_categories: exclude_categories, include_characters: include_characters,
127
+ exclude_characters: exclude_characters
128
+ )
93
129
  end
94
130
 
95
- # A byte String of [min_size, max_size] bytes.
131
+ # A byte String of [min_size, max_size] bytes; max_size defaults as
132
+ # in #text.
96
133
  def binary(min_size: 0, max_size: nil)
97
134
  Generators::BinaryGenerator.new(min_size: min_size, max_size: max_size)
98
135
  end
99
136
 
100
137
  # A String matching +pattern+ (Python `re` syntax, not Ruby's
101
138
  # Regexp syntax; see Generators::FromRegexGenerator). fullmatch
102
- # requires the whole string to match, not just contain a match.
103
- def from_regex(pattern, fullmatch: false)
104
- Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch)
139
+ # (the default) requires the whole string to match, not just contain
140
+ # a match.
141
+ # +alphabet+ is a #characters generator limiting every character of the
142
+ # string.
143
+ def from_regex(pattern, fullmatch: true, alphabet: nil)
144
+ Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch, alphabet: alphabet)
145
+ end
146
+
147
+ # A String that +grammar+, written in the syntax of the Lark parsing
148
+ # library, accepts from its +start+ rule. +explicit+ maps terminal names
149
+ # to generators of the values drawn for them, +alphabet+ is a
150
+ # #characters generator limiting what terminal patterns draw, and
151
+ # +import_path+ is the directory %import reads files from.
152
+ def from_lark(grammar, start: nil, explicit: {}, alphabet: nil, max_size: nil, max_depth: nil, import_path: nil)
153
+ Generators::FromLarkGenerator.new(
154
+ grammar, start: start, explicit: explicit, alphabet: alphabet, max_size: max_size, max_depth: max_depth,
155
+ import_path: import_path
156
+ )
105
157
  end
106
158
 
107
159
  # An RFC 5321/5322 email address String.
@@ -152,6 +204,29 @@ module Hegel
152
204
  Generators::DatetimesGenerator.new(min_value: min_value, max_value: max_value)
153
205
  end
154
206
 
207
+ # A "YYYY-MM-DD" String, years 1 through 9999. Takes no options.
208
+ def date_strings
209
+ Generators::DateStringsGenerator.new
210
+ end
211
+
212
+ # An "HH:MM:SS" String, with ".fffffffff" appended when the
213
+ # nanosecond is not zero. Takes no options.
214
+ def time_strings
215
+ Generators::TimeStringsGenerator.new
216
+ end
217
+
218
+ # A "YYYY-MM-DDTHH:MM:SS[.fffffffff]" String, years 1 through 9999.
219
+ # Takes no options.
220
+ def datetime_strings
221
+ Generators::DatetimeStringsGenerator.new
222
+ end
223
+
224
+ # A duration as Integer nanoseconds in [min_value, max_value], up to
225
+ # 2**64 - 1 (see Generators::DurationsGenerator and docs/adr/0030).
226
+ def durations(min_value: 0, max_value: (2**64) - 1)
227
+ Generators::DurationsGenerator.new(min_value: min_value, max_value: max_value)
228
+ end
229
+
155
230
  # A value built from imperative code: +block+ receives a draw
156
231
  # surface and may call #draw on it any number of times to assemble
157
232
  # one value (see Generators::CompositeGenerator).
@@ -165,6 +240,15 @@ module Hegel
165
240
  def deferred
166
241
  Generators::DeferredGenerator.new
167
242
  end
243
+
244
+ # A recursively defined value, such as a tree: either a value from
245
+ # +leaf+, or what the block's generator builds from sub-values of the
246
+ # same kind (see Generators::RecursiveGenerator). The block receives
247
+ # the generator of those sub-values and is called afresh for each
248
+ # branch.
249
+ def recursive(leaf, max_depth: 32, max_leaves: 100, &branch)
250
+ Generators::RecursiveGenerator.new(leaf, max_depth: max_depth, max_leaves: max_leaves, &branch)
251
+ end
168
252
  end
169
253
  end
170
254
 
@@ -46,6 +46,16 @@ module Hegel
46
46
  # loudly.
47
47
  DRAW_CALLER_DEPTH = 3
48
48
 
49
+ # The largest minimum length #repeat draws for its loop: 2**40, the
50
+ # bound hegel-rust's TestCase::repeat draws its own minimum under
51
+ # (max_safe_min_size).
52
+ REPEAT_MAX_MIN_SIZE = 2**40
53
+
54
+ # How many columns further #repeat indents the entries one iteration
55
+ # records: the indent hegel-rust's TestCase::repeat gives the block
56
+ # handle it runs each iteration under.
57
+ REPEAT_INDENT = 2
58
+
49
59
  # +impl+ and +ctx+ are carried alongside +handle+ so each draw call can
50
60
  # reach the same LibHegel implementation and context the run loop opened,
51
61
  # without this class knowing anything about the native binding layer or
@@ -54,17 +64,26 @@ module Hegel
54
64
  # +record+ defaults to false: recording is Hegel::Runner's decision, made
55
65
  # only for the one, already-shrunk replay that produces a failure report
56
66
  # (see #record_draw for why every other iteration skips it).
57
- def initialize(impl, ctx, handle, record: false)
67
+ #
68
+ # +grammars+ is the run's Hegel::GrammarCache, which Hegel::Runner
69
+ # passes; a test case built without one compiles a grammar for each draw.
70
+ def initialize(impl, ctx, handle, record: false, grammars: nil)
58
71
  @impl = impl
59
72
  @ctx = ctx
60
73
  @handle = handle
61
74
  @record = record
62
75
  @entries = record ? [] : nil
63
76
  @pools = []
77
+ @grammars = grammars
78
+ # Only declares the ivar: an unset one already reads as nil.
79
+ @poison = nil # mutineer:disable-line statement_removal
80
+ @indent = 0
64
81
  end
65
82
 
66
- # The [:draw, name, value] / [:note, message] entries recorded so far, in
67
- # call order, or nil when this instance was not built to record.
83
+ # The [:draw, name, value, indent] / [:note, message, indent] entries
84
+ # recorded so far, in call order, or nil when this instance was not
85
+ # built to record. +indent+ is how many columns further in the report
86
+ # puts the entry; #repeat raises it around each iteration.
68
87
  # Hegel::Runner reads this once, after the block that owns this test
69
88
  # case has run to completion or raised. One tagged list rather than a
70
89
  # draws list and a separate notes list, so a note recorded between two
@@ -151,21 +170,56 @@ module Hegel
151
170
 
152
171
  return unless @record
153
172
 
154
- @entries << [:note, has_message ? message : yield]
173
+ @entries << [:note, has_message ? message : yield, @indent]
174
+ end
175
+
176
+ # Runs the block over and over, for as long as the engine keeps the
177
+ # loop going, and never returns normally: when the engine ends the loop
178
+ # it raises Hegel::LoopDone, which Hegel::Runner.classify reports as a
179
+ # passing case. A `break` in the block ends the loop early and returns
180
+ # from this method instead; hegel-rust's TestCase::repeat has no
181
+ # counterpart, since a Rust closure cannot break its caller's loop.
182
+ # docs/adr/0034 records both decisions.
183
+ #
184
+ # The loop is a collection whose minimum length is drawn first, from 0
185
+ # up to REPEAT_MAX_MIN_SIZE, with no maximum, the way hegel-rust draws
186
+ # it. Each iteration notes "// Repetition #N", then records what the
187
+ # block records REPEAT_INDENT columns further in. An iteration whose
188
+ # assumption failed is skipped rather than discarding the whole case;
189
+ # any other exception ends the loop (see #repeat_iteration).
190
+ #
191
+ # The minimum length is drawn with no span around it. hegel-rust's
192
+ # draw_silent opens one around every draw, and this library opens a
193
+ # span only around a generator that makes more than one native call
194
+ # (.claude/skills/new-generator), which one generate_integer is not.
195
+ def repeat(&block)
196
+ min_size = generate_integer(0, REPEAT_MAX_MIN_SIZE)
197
+ collection = new_collection(min_size, LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED)
198
+ begin
199
+ iteration = 0
200
+ while collection_more(collection)
201
+ iteration += 1
202
+ note { "// Repetition ##{iteration}" }
203
+ repeat_iteration(&block)
204
+ end
205
+ ensure
206
+ collection_free(collection)
207
+ end
208
+ raise Hegel::LoopDone, "hegel: repeat finished its loop"
155
209
  end
156
210
 
157
211
  # hegel_generate_integer, without recording. Hegel::Generators::
158
212
  # IntegerGenerator's own primitive; #draw_integer is this plus
159
213
  # recording.
160
214
  def generate_integer(min_value, max_value)
161
- @impl.generate_integer(@ctx, @handle, min_value, max_value)
215
+ @impl.generate_integer(@ctx, live_handle, min_value, max_value)
162
216
  end
163
217
 
164
218
  # hegel_generate_integer_big, without recording. Hegel::Generators::
165
219
  # IntegerGenerator's own primitive for bounds outside int64_t's range,
166
220
  # used instead of #generate_integer only then; see IntegerGenerator#do_draw.
167
221
  def generate_integer_big(min_value, max_value)
168
- @impl.generate_integer_big(@ctx, @handle, min_value, max_value)
222
+ @impl.generate_integer_big(@ctx, live_handle, min_value, max_value)
169
223
  end
170
224
 
171
225
  # hegel_generate_boolean, without recording. Hegel::Generators::
@@ -175,14 +229,14 @@ module Hegel
175
229
  # has_forced is false, so the engine ignores the forced argument's value.
176
230
  # .mutineer.yml ignores that mutant by id, since a line marker would also
177
231
  # hide the killed mutant of has_forced.
178
- @impl.generate_boolean(@ctx, @handle, p, false, false)
232
+ @impl.generate_boolean(@ctx, live_handle, p, false, false)
179
233
  end
180
234
 
181
235
  # hegel_generate_float. Hegel::Generators::FloatGenerator's own
182
236
  # primitive.
183
237
  def generate_float(width, min_value, max_value, allow_nan:, allow_infinity:, exclude_min:, exclude_max:,
184
238
  smallest_nonzero_magnitude:)
185
- @impl.generate_float(@ctx, @handle, width, min_value, max_value, allow_nan, allow_infinity, exclude_min,
239
+ @impl.generate_float(@ctx, live_handle, width, min_value, max_value, allow_nan, allow_infinity, exclude_min,
186
240
  exclude_max, smallest_nonzero_magnitude)
187
241
  end
188
242
 
@@ -220,33 +274,49 @@ module Hegel
220
274
  # this class makes. Writing the same check on this side would only
221
275
  # give one mistake two messages to drift apart from each other.
222
276
  def target(value, label: "")
223
- @impl.target(@ctx, @handle, value, label)
277
+ @impl.target(@ctx, live_handle, value, label)
278
+ end
279
+
280
+ def event(label)
281
+ @impl.event(@ctx, live_handle, label)
282
+ end
283
+
284
+ # No finite check here: hegel_event_value's HEGEL_E_INVALID_ARG reaches
285
+ # the caller through LibHegel.check!, as it does for #target.
286
+ def event_value(label, value)
287
+ @impl.event_value(@ctx, live_handle, label, value)
224
288
  end
225
289
 
226
290
  # hegel_start_span, labelled with a generator's label (see
227
291
  # Hegel::Generator#label). Every compound Hegel::Generator (map, filter,
228
292
  # arrays) opens one of these around its own draw; see #stop_span.
229
293
  def start_span(label)
230
- @impl.start_span(@ctx, @handle, label)
294
+ @impl.start_span(@ctx, live_handle, label)
231
295
  end
232
296
 
233
297
  # hegel_stop_span, closing the span #start_span most recently opened.
234
298
  # +discard+ true marks it rejected (a filter predicate that did not
235
299
  # hold), so libhegel retries from before the span opened.
300
+ #
301
+ # Makes no native call while an abandoned recursive attempt unwinds:
302
+ # hegel_recursion_retry discards the spans that attempt left open, and
303
+ # closing them here first would keep them. See docs/adr/0032.
236
304
  def stop_span(discard: false)
305
+ return if @poison
306
+
237
307
  @impl.stop_span(@ctx, @handle, discard)
238
308
  end
239
309
 
240
310
  # hegel_new_collection: Hegel::Generators::ArrayGenerator's own sizing
241
311
  # primitive, paired with #collection_more and #collection_free.
242
312
  def new_collection(min_size, max_size)
243
- @impl.new_collection(@ctx, @handle, min_size, max_size)
313
+ @impl.new_collection(@ctx, live_handle, min_size, LibHegel.max_size_arg(max_size))
244
314
  end
245
315
 
246
316
  # hegel_collection_more: whether to draw another element into
247
317
  # +collection+.
248
318
  def collection_more(collection)
249
- @impl.collection_more(@ctx, @handle, collection)
319
+ @impl.collection_more(@ctx, live_handle, collection)
250
320
  end
251
321
 
252
322
  # hegel_collection_reject: tells libhegel the element most recently
@@ -255,7 +325,7 @@ module Hegel
255
325
  # #collection_more call offers another attempt at the same slot instead
256
326
  # of treating the collection as one element closer to done.
257
327
  def collection_reject(collection, why: nil)
258
- @impl.collection_reject(@ctx, @handle, collection, why)
328
+ @impl.collection_reject(@ctx, live_handle, collection, why)
259
329
  end
260
330
 
261
331
  # hegel_collection_free.
@@ -270,15 +340,17 @@ module Hegel
270
340
  # read the same way as #with_regex_generator/#with_email_generator/
271
341
  # #with_url_generator/#with_domain_generator below, each named for its
272
342
  # own hegel_string_generator_* call.
273
- def with_text_generator(**kwargs, &block)
274
- with_generator_handle(@impl.string_generator_text(@ctx, **kwargs), &block)
343
+ def with_text_generator(max_size:, **kwargs, &block)
344
+ handle = @impl.string_generator_text(@ctx, max_size: LibHegel.max_size_arg(max_size), **kwargs)
345
+ with_generator_handle(handle, &block)
275
346
  end
276
347
 
277
348
  # Runs the block with a regex-matching string generator handle built
278
- # from +pattern+/+fullmatch+ (see #string_generator_regex), freeing it
279
- # via #with_generator_handle whether the block returns or raises.
280
- def with_regex_generator(pattern, fullmatch:, &block)
281
- with_generator_handle(string_generator_regex(pattern, fullmatch), &block)
349
+ # from +pattern+/+fullmatch+/+alphabet+ (see #string_generator_regex),
350
+ # freeing it via #with_generator_handle whether the block returns or
351
+ # raises. +alphabet+ stays the caller's to free.
352
+ def with_regex_generator(pattern, fullmatch:, alphabet: nil, &block)
353
+ with_generator_handle(string_generator_regex(pattern, fullmatch, alphabet), &block)
282
354
  end
283
355
 
284
356
  # Runs the block with an email-address string generator handle (see
@@ -304,26 +376,26 @@ module Hegel
304
376
  # #with_regex_generator, #with_email_generator, #with_url_generator, or
305
377
  # #with_domain_generator).
306
378
  def generate_string(generator)
307
- @impl.generate_string(@ctx, @handle, generator)
379
+ @impl.generate_string(@ctx, live_handle, generator)
308
380
  end
309
381
 
310
382
  # hegel_generate_bytes. A future Hegel::Generators::BinaryGenerator's
311
383
  # own primitive, the bytes counterpart to #generate_string.
312
384
  def generate_bytes(min_size, max_size)
313
- @impl.generate_bytes(@ctx, @handle, min_size, max_size)
385
+ @impl.generate_bytes(@ctx, live_handle, min_size, LibHegel.max_size_arg(max_size))
314
386
  end
315
387
 
316
388
  # hegel_generate_ipv4, returning the address's 4 raw bytes. Converting
317
389
  # to a caller-facing address type (IPAddr or similar) is left to the
318
390
  # generator built on top of this call.
319
391
  def generate_ipv4
320
- @impl.generate_ipv4(@ctx, @handle)
392
+ @impl.generate_ipv4(@ctx, live_handle)
321
393
  end
322
394
 
323
395
  # hegel_generate_ipv6, returning the address's 16 raw bytes. Same
324
396
  # division of labor as #generate_ipv4.
325
397
  def generate_ipv6
326
- @impl.generate_ipv6(@ctx, @handle)
398
+ @impl.generate_ipv6(@ctx, live_handle)
327
399
  end
328
400
 
329
401
  # hegel_generate_uuid, returning 16 raw bytes. Hegel::Generators::
@@ -331,7 +403,7 @@ module Hegel
331
403
  # hex String is that generator's job, the same division of labor
332
404
  # #generate_ipv4/#generate_ipv6 already follow.
333
405
  def generate_uuid(version, has_version)
334
- @impl.generate_uuid(@ctx, @handle, version, has_version)
406
+ @impl.generate_uuid(@ctx, live_handle, version, has_version)
335
407
  end
336
408
 
337
409
  # hegel_string_generator_regex. +alphabet+ is an optional string
@@ -385,7 +457,7 @@ module Hegel
385
457
  # Hegel::Stateful::Pool.new cannot forget it and leak the handle --
386
458
  # docs/adr/0011 has the ownership decision behind this.
387
459
  def new_pool
388
- pool = @impl.new_pool(@ctx, @handle)
460
+ pool = @impl.new_pool(@ctx, live_handle)
389
461
  @pools << pool
390
462
  pool
391
463
  end
@@ -394,7 +466,7 @@ module Hegel
394
466
  # associate with the value it just generated. Hegel::Stateful::Pool#add's
395
467
  # own primitive.
396
468
  def pool_add(pool)
397
- @impl.pool_add(@ctx, @handle, pool)
469
+ @impl.pool_add(@ctx, live_handle, pool)
398
470
  end
399
471
 
400
472
  # hegel_pool_generate: the variable id libhegel chose from +pool+ (and
@@ -405,7 +477,13 @@ module Hegel
405
477
  # translates that to Hegel::AssumeFailed, the same as every other
406
478
  # assumption failure.
407
479
  def pool_generate(pool, consume)
408
- @impl.pool_generate(@ctx, @handle, pool, consume)
480
+ @impl.pool_generate(@ctx, live_handle, pool, consume)
481
+ end
482
+
483
+ # hegel_pool_restore: puts back +variable_id+, a consuming
484
+ # #pool_generate's answer that the caller is throwing away unused.
485
+ def pool_restore(pool, variable_id)
486
+ @impl.pool_restore(@ctx, live_handle, pool, variable_id)
409
487
  end
410
488
 
411
489
  # hegel_pool_free. Not called directly by a caller of
@@ -431,34 +509,34 @@ module Hegel
431
509
  # here rather than on that module, the same reason every other native
432
510
  # call is a method on this class: Hegel::Stateful never touches @impl or
433
511
  # @ctx directly, only this handle-scoped surface.
434
- def new_state_machine(rule_names, invariant_names, always_check, step_count)
435
- @impl.new_state_machine(@ctx, @handle, rule_names, invariant_names, always_check, step_count)
512
+ def new_state_machine(rule_names, rule_weights, invariant_names, always_check, step_count)
513
+ @impl.new_state_machine(@ctx, live_handle, rule_names, rule_weights, invariant_names, always_check, step_count)
436
514
  end
437
515
 
438
516
  # hegel_state_machine_next_group: starts the next round, or returns
439
517
  # LibHegel::HEGEL_STATE_MACHINE_DONE once the machine is done.
440
518
  def state_machine_next_group(state_machine)
441
- @impl.state_machine_next_group(@ctx, @handle, state_machine)
519
+ @impl.state_machine_next_group(@ctx, live_handle, state_machine)
442
520
  end
443
521
 
444
522
  # hegel_state_machine_next_rule: the index (into the +rule_names+
445
523
  # #new_state_machine was given) of the next rule to run this round, or
446
524
  # LibHegel::HEGEL_STATE_MACHINE_DONE once the round is over.
447
525
  def state_machine_next_rule(state_machine)
448
- @impl.state_machine_next_rule(@ctx, @handle, state_machine)
526
+ @impl.state_machine_next_rule(@ctx, live_handle, state_machine)
449
527
  end
450
528
 
451
529
  # hegel_state_machine_rule_rejected: tells libhegel the rule most
452
530
  # recently returned by #state_machine_next_rule stopped early on a
453
531
  # failed assumption, so its round does not count toward the step budget.
454
532
  def state_machine_rule_rejected(state_machine)
455
- @impl.state_machine_rule_rejected(@ctx, @handle, state_machine)
533
+ @impl.state_machine_rule_rejected(@ctx, live_handle, state_machine)
456
534
  end
457
535
 
458
536
  # hegel_state_machine_should_check_invariant: whether to run the
459
537
  # invariant at +index+ at this join point.
460
538
  def state_machine_should_check_invariant(state_machine, index)
461
- @impl.state_machine_should_check_invariant(@ctx, @handle, state_machine, index)
539
+ @impl.state_machine_should_check_invariant(@ctx, live_handle, state_machine, index)
462
540
  end
463
541
 
464
542
  # hegel_state_machine_free. Takes no test-case handle, unlike every
@@ -470,11 +548,69 @@ module Hegel
470
548
  @impl.state_machine_free(@ctx, state_machine)
471
549
  end
472
550
 
551
+ # A hegel_new_recursion handle, and whether #recursion_free has
552
+ # released it.
553
+ Recursion = Struct.new(:handle, :freed)
554
+
555
+ # hegel_new_recursion. Hegel::Generators::RecursiveGenerator's own
556
+ # scope, freed by #recursion_free in that generator's `ensure`.
557
+ def new_recursion(max_depth, max_leaves)
558
+ Recursion.new(@impl.new_recursion(@ctx, live_handle, max_depth, max_leaves), false)
559
+ end
560
+
561
+ # hegel_recursion_branch: whether the sub-value at +depth+ is a branch.
562
+ def recursion_branch(recursion, depth)
563
+ @impl.recursion_branch(@ctx, live_handle, recursion.handle, depth)
564
+ end
565
+
566
+ # hegel_recursion_leaf. Raises Hegel::LeafBudgetExceeded when the
567
+ # attempt outgrew its leaf budget, and keeps it until #recursion_retry:
568
+ # every call that takes the test-case handle raises it again instead of
569
+ # reaching the engine, so a body that rescues it cannot draw on into an
570
+ # attempt the engine is about to discard.
571
+ def recursion_leaf(recursion)
572
+ return if @impl.recursion_leaf(@ctx, live_handle, recursion.handle)
573
+
574
+ @poison = LeafBudgetExceeded.new(recursion)
575
+ raise @poison
576
+ end
577
+
578
+ # True while the attempt +recursion+ abandoned through #recursion_leaf
579
+ # still waits for #recursion_retry.
580
+ def abandoned_by?(recursion)
581
+ @poison.is_a?(LeafBudgetExceeded) && @poison.recursion.equal?(recursion)
582
+ end
583
+
584
+ # hegel_recursion_retry, which reaches the engine with the raw handle
585
+ # while an attempt is abandoned. HEGEL_E_ASSUME or HEGEL_E_STOP_TEST ends the
586
+ # test case, so its exception takes the abandoned attempt's place and
587
+ # later calls raise it without reaching the engine.
588
+ def recursion_retry(recursion)
589
+ @impl.recursion_retry(@ctx, @handle, recursion.handle)
590
+ @poison = nil
591
+ rescue AssumeFailed, StopTest => e
592
+ @poison = e
593
+ raise
594
+ end
595
+
596
+ # hegel_recursion_finish: false when the engine discarded the finished
597
+ # attempt, which the caller then draws again from the root.
598
+ def recursion_finish(recursion)
599
+ @impl.recursion_finish(@ctx, live_handle, recursion.handle)
600
+ end
601
+
602
+ # hegel_recursion_free. Takes no test-case handle, so it runs while an
603
+ # attempt is abandoned.
604
+ def recursion_free(recursion)
605
+ recursion.freed = true
606
+ @impl.recursion_free(@ctx, recursion.handle)
607
+ end
608
+
473
609
  # hegel_generate_date, without recording. Hegel::Generators::
474
610
  # DatesGenerator's own primitive. +min_value+/+max_value+ are each a
475
611
  # [year, month, day] Array; the return value is the same shape.
476
612
  def generate_date(min_value, max_value)
477
- @impl.generate_date(@ctx, @handle, min_value, max_value)
613
+ @impl.generate_date(@ctx, live_handle, min_value, max_value)
478
614
  end
479
615
 
480
616
  # hegel_generate_time, without recording. Hegel::Generators::
@@ -482,7 +618,7 @@ module Hegel
482
618
  # [hour, minute, second, microsecond] Array; the return value is the
483
619
  # same shape.
484
620
  def generate_time(min_value, max_value)
485
- @impl.generate_time(@ctx, @handle, min_value, max_value)
621
+ @impl.generate_time(@ctx, live_handle, min_value, max_value)
486
622
  end
487
623
 
488
624
  # hegel_generate_datetime, without recording. Hegel::Generators::
@@ -491,11 +627,97 @@ module Hegel
491
627
  # [hour, minute, second, microsecond] Array; the return value is a
492
628
  # [[year, month, day], [hour, minute, second, microsecond]] pair.
493
629
  def generate_datetime(min_date, min_time, max_date, max_time)
494
- @impl.generate_datetime(@ctx, @handle, min_date, min_time, max_date, max_time)
630
+ @impl.generate_datetime(@ctx, live_handle, min_date, min_time, max_date, max_time)
631
+ end
632
+
633
+ # Yields the Hegel::GrammarCache::Compiled under +key+ in the run's
634
+ # cache, calling +build+ to compile it on the first draw. Without a
635
+ # cache, it compiles one for this draw alone and frees it afterwards.
636
+ def with_grammar(key, build)
637
+ return yield @grammars.fetch(key, &build) if @grammars
638
+
639
+ compiled = build.call
640
+ begin
641
+ yield compiled
642
+ ensure
643
+ grammar_free(compiled.grammar)
644
+ end
645
+ end
646
+
647
+ def grammar_builder_new(text)
648
+ @impl.grammar_builder_new(@ctx, text)
649
+ end
650
+
651
+ def grammar_builder_file(builder, index)
652
+ @impl.grammar_builder_file(@ctx, builder, index)
653
+ end
654
+
655
+ def grammar_builder_provide(builder, index, text)
656
+ @impl.grammar_builder_provide(@ctx, builder, index, text)
657
+ end
658
+
659
+ def grammar_builder_free(builder)
660
+ @impl.grammar_builder_free(@ctx, builder)
661
+ end
662
+
663
+ def grammar_new(builder, start, max_size, max_depth, alphabet, names)
664
+ @impl.grammar_new(@ctx, builder, start, max_size, max_depth, alphabet, names)
665
+ end
666
+
667
+ def grammar_placeholder(grammar, name)
668
+ @impl.grammar_placeholder(@ctx, grammar, name)
669
+ end
670
+
671
+ def grammar_free(grammar)
672
+ @impl.grammar_free(@ctx, grammar)
673
+ end
674
+
675
+ # hegel_generate_grammar, without recording.
676
+ def generate_grammar(grammar, max_size)
677
+ @impl.generate_grammar(@ctx, live_handle, grammar, max_size)
495
678
  end
496
679
 
497
680
  private
498
681
 
682
+ def live_handle
683
+ raise @poison if @poison
684
+
685
+ @handle
686
+ end
687
+
688
+ # Runs one #repeat iteration with the indent raised by REPEAT_INDENT.
689
+ # hegel-rust indents an iteration's output through hegel_test_case_block,
690
+ # which indents only what the engine printer prints. This library
691
+ # renders its report itself (Hegel::Report, docs/adr/0005), so the
692
+ # indent is kept on this side and recorded with each entry.
693
+ #
694
+ # The rescue order follows hegel-rust's TestCase::repeat, after the
695
+ # fatal exceptions every `rescue Exception` here lets through first. The
696
+ # library's own control exceptions, a nested #repeat's Hegel::LoopDone
697
+ # among them, and Hegel::Error, which stands where hegel-rust's
698
+ # InvalidArgument and InternalError stand, re-raise unchanged. A failed
699
+ # assumption skips to the next iteration. Anything else is a failure:
700
+ # one boolean is drawn before it re-raises, as hegel-rust draws one.
701
+ #
702
+ # standard:disable Lint/RescueException -- the same reason as
703
+ # Hegel::Runner.classify: a failing assertion descends from Exception.
704
+ def repeat_iteration
705
+ @indent += REPEAT_INDENT
706
+ begin
707
+ yield
708
+ ensure
709
+ @indent -= REPEAT_INDENT
710
+ end
711
+ rescue *Hegel::FATAL_EXCEPTIONS, Hegel::StopTest, Hegel::LoopDone, Hegel::Error
712
+ raise
713
+ rescue Hegel::AssumeFailed
714
+ # Skips this iteration; #repeat goes on to the next one.
715
+ rescue Exception
716
+ generate_boolean
717
+ raise
718
+ end
719
+ # standard:enable Lint/RescueException
720
+
499
721
  # Runs the block with +generator+, freeing it via #string_generator_free
500
722
  # whether the block returns or raises. The one #ensure
501
723
  # #with_regex_generator/#with_email_generator/#with_url_generator/
@@ -522,7 +744,7 @@ module Hegel
522
744
  def record_draw(label, value)
523
745
  return unless @record
524
746
 
525
- @entries << [:draw, name_for(label, DRAW_CALLER_DEPTH), value]
747
+ @entries << [:draw, name_for(label, DRAW_CALLER_DEPTH), value, @indent]
526
748
  end
527
749
 
528
750
  # The single place a draw's name is decided, tried in this order: the