hegeltest 0.3.4 → 0.4.1

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 (39) 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_version.rb +1 -1
  26. data/lib/hegel/report.rb +13 -10
  27. data/lib/hegel/runner.rb +61 -27
  28. data/lib/hegel/settings.rb +92 -16
  29. data/lib/hegel/state_machine.rb +17 -4
  30. data/lib/hegel/stateful/pool.rb +52 -0
  31. data/lib/hegel/stateful.rb +2 -2
  32. data/lib/hegel/syntax/methods.rb +99 -15
  33. data/lib/hegel/test_case.rb +259 -37
  34. data/lib/hegel/test_location.rb +49 -0
  35. data/lib/hegel/version.rb +1 -1
  36. data/lib/hegel.rb +21 -5
  37. data/sig/hegel.rbs +300 -42
  38. data/skills/hegel-ruby/references/ruby/reference.md +463 -41
  39. metadata +15 -1
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hegel
4
+ # The grammars compiled during one run, keyed by everything the
5
+ # compilation read. Hegel::Runner.run owns one and frees it after the run
6
+ # and before the context; see docs/adr/0033.
7
+ class GrammarCache
8
+ # A compiled grammar handle and the marker standing for each explicit
9
+ # terminal, in the order the terminals were named.
10
+ Compiled = Data.define(:grammar, :markers)
11
+
12
+ def initialize
13
+ @entries = {}
14
+ end
15
+
16
+ # The entry under +key+, or the block's value, stored only once the
17
+ # block returns, so a compilation that raises leaves nothing behind.
18
+ def fetch(key)
19
+ @entries.fetch(key) { @entries[key] = yield }
20
+ end
21
+
22
+ # Frees every grammar, carrying on past a free that raises so that the
23
+ # rest are still freed before the context, and raises the first error
24
+ # once all have been tried.
25
+ def free_all(impl, ctx)
26
+ error = nil
27
+ @entries.each_value do |compiled|
28
+ impl.grammar_free(ctx, compiled.grammar)
29
+ rescue => e
30
+ error ||= e
31
+ end
32
+ @entries.clear
33
+ raise error if error
34
+ end
35
+ end
36
+ end
@@ -82,6 +82,14 @@ module Hegel
82
82
 
83
83
  @hegel_settings_new_fn = bind("hegel_settings_new", [:pointer, :pointer], :int32)
84
84
  @hegel_settings_free_fn = bind("hegel_settings_free", [:pointer, :pointer], :int32)
85
+ @hegel_settings_new_for_profile_fn = bind(
86
+ "hegel_settings_new_for_profile", [:pointer, StringStruct.by_value, :pointer], :int32
87
+ )
88
+ @hegel_settings_register_profile_fn = bind(
89
+ "hegel_settings_register_profile", [:pointer, StringStruct.by_value, :pointer], :int32
90
+ )
91
+ @hegel_set_default_profile_fn = bind("hegel_set_default_profile", [:pointer, StringStruct.by_value], :int32)
92
+ @hegel_set_unicode_version_fn = bind("hegel_set_unicode_version", [:pointer, StringStruct.by_value], :int32)
85
93
  @hegel_settings_set_test_cases_fn = bind(
86
94
  "hegel_settings_set_test_cases", [:pointer, :pointer, :uint64], :int32
87
95
  )
@@ -109,6 +117,24 @@ module Hegel
109
117
  @hegel_settings_set_suppress_health_check_fn = bind(
110
118
  "hegel_settings_set_suppress_health_check", [:pointer, :pointer, :uint32], :int32
111
119
  )
120
+ @hegel_settings_set_stop_on_first_failure_fn = bind(
121
+ "hegel_settings_set_stop_on_first_failure", [:pointer, :pointer, :bool], :int32
122
+ )
123
+ @hegel_settings_set_print_blob_fn = bind("hegel_settings_set_print_blob", [:pointer, :pointer, :bool], :int32)
124
+ @hegel_settings_get_print_blob_fn = bind(
125
+ "hegel_settings_get_print_blob", [:pointer, :pointer, :pointer], :int32
126
+ )
127
+ @hegel_settings_set_show_statistics_fn = bind(
128
+ "hegel_settings_set_show_statistics", [:pointer, :pointer, :bool], :int32
129
+ )
130
+ @hegel_settings_set_nondeterminism_strictness_fn = bind(
131
+ "hegel_settings_set_nondeterminism_strictness", [:pointer, :pointer, :uint32], :int32
132
+ )
133
+ @hegel_settings_set_backend_fn = bind("hegel_settings_set_backend", [:pointer, :pointer, :uint32], :int32)
134
+ @hegel_settings_set_test_location_fn = bind(
135
+ "hegel_settings_set_test_location",
136
+ [:pointer, :pointer, StringStruct.by_value, :uint32, StringStruct.by_value, StringStruct.by_value], :int32
137
+ )
112
138
 
113
139
  @hegel_run_start_fn = bind(
114
140
  "hegel_run_start", [:pointer, :pointer, :pointer, :pointer, :pointer], :int32
@@ -124,6 +150,10 @@ module Hegel
124
150
  @hegel_test_case_free_fn = bind("hegel_test_case_free", [:pointer, :pointer], :int32)
125
151
  @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :pointer], :int32)
126
152
  @hegel_target_fn = bind("hegel_target", [:pointer, :pointer, :double, StringStruct.by_value], :int32)
153
+ @hegel_event_fn = bind("hegel_event", [:pointer, :pointer, StringStruct.by_value], :int32)
154
+ @hegel_event_value_fn = bind(
155
+ "hegel_event_value", [:pointer, :pointer, :double, StringStruct.by_value], :int32
156
+ )
127
157
 
128
158
  @hegel_run_result_fn = bind("hegel_run_result", [:pointer, :pointer, :pointer], :int32)
129
159
  @hegel_run_result_free_fn = bind("hegel_run_result_free", [:pointer, :pointer], :int32)
@@ -170,8 +200,20 @@ module Hegel
170
200
  @hegel_pool_generate_fn = bind(
171
201
  "hegel_pool_generate", [:pointer, :pointer, :pointer, :bool, :pointer], :int32
172
202
  )
203
+ @hegel_pool_restore_fn = bind("hegel_pool_restore", [:pointer, :pointer, :pointer, :int64], :int32)
173
204
  @hegel_pool_free_fn = bind("hegel_pool_free", [:pointer, :pointer], :int32)
174
205
 
206
+ @hegel_new_recursion_fn = bind(
207
+ "hegel_new_recursion", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
208
+ )
209
+ @hegel_recursion_branch_fn = bind(
210
+ "hegel_recursion_branch", [:pointer, :pointer, :pointer, :uint64, :pointer], :int32
211
+ )
212
+ @hegel_recursion_leaf_fn = bind("hegel_recursion_leaf", [:pointer, :pointer, :pointer], :int32)
213
+ @hegel_recursion_retry_fn = bind("hegel_recursion_retry", [:pointer, :pointer, :pointer], :int32)
214
+ @hegel_recursion_finish_fn = bind("hegel_recursion_finish", [:pointer, :pointer, :pointer], :int32)
215
+ @hegel_recursion_free_fn = bind("hegel_recursion_free", [:pointer, :pointer], :int32)
216
+
175
217
  @hegel_new_state_machine_fn = bind(
176
218
  "hegel_new_state_machine",
177
219
  [
@@ -198,10 +240,6 @@ module Hegel
198
240
  [:pointer, :pointer, :uint32, :double, :double, :bool, :bool, :bool, :bool, :double, :pointer], :int32
199
241
  )
200
242
 
201
- # hegel_string_generator_text takes 12 arguments past ctx; the
202
- # last 6 (categories through exclude_characters) are the category
203
- # and explicit-character filter parameters this binding does not
204
- # wire up (see #string_generator_text).
205
243
  @hegel_string_generator_text_fn = bind(
206
244
  "hegel_string_generator_text",
207
245
  [:pointer, :uint64, :uint64, :pointer, :uint32, :uint32,
@@ -248,7 +286,29 @@ module Hegel
248
286
  [:pointer, :pointer, DatetimeStruct.by_value, DatetimeStruct.by_value, :pointer], :int32
249
287
  )
250
288
 
251
- LibHegel.with_context(self) { |ctx| LibHegel.warn_on_version_mismatch(self, ctx, io: io) }
289
+ @hegel_grammar_builder_new_fn = bind(
290
+ "hegel_grammar_builder_new", [:pointer, StringStruct.by_value, :pointer], :int32
291
+ )
292
+ @hegel_grammar_builder_file_fn = bind(
293
+ "hegel_grammar_builder_file", [:pointer, :pointer, :size_t, :pointer], :int32
294
+ )
295
+ @hegel_grammar_builder_provide_fn = bind(
296
+ "hegel_grammar_builder_provide", [:pointer, :pointer, :size_t, :pointer], :int32
297
+ )
298
+ @hegel_grammar_builder_free_fn = bind("hegel_grammar_builder_free", [:pointer, :pointer], :int32)
299
+ @hegel_grammar_new_fn = bind(
300
+ "hegel_grammar_new",
301
+ [:pointer, :pointer, :pointer, :uint64, :uint32, :pointer, :pointer, :size_t, :pointer], :int32
302
+ )
303
+ @hegel_grammar_placeholder_fn = bind(
304
+ "hegel_grammar_placeholder", [:pointer, :pointer, StringStruct.by_value, :pointer], :int32
305
+ )
306
+ @hegel_grammar_free_fn = bind("hegel_grammar_free", [:pointer, :pointer], :int32)
307
+ @hegel_generate_grammar_fn = bind(
308
+ "hegel_generate_grammar", [:pointer, :pointer, :pointer, :uint64, :pointer], :int32
309
+ )
310
+
311
+ LibHegel.with_context(self) { |ctx| LibHegel.prepare(self, ctx, io: io) }
252
312
  end
253
313
 
254
314
  # hegel_context_new never returns NULL (guaranteed by the header), so
@@ -298,6 +358,39 @@ module Hegel
298
358
  out.read_pointer
299
359
  end
300
360
 
361
+ # Like #settings_new, but starts from the profile +name+, which still
362
+ # extends the default profile (except "base"). An unknown name returns
363
+ # HEGEL_E_INVALID_ARG.
364
+ def settings_new_for_profile(ctx, name)
365
+ out = FFI::MemoryPointer.new(:pointer)
366
+ code = @hegel_settings_new_for_profile_fn.call(ctx, string_view(name), out)
367
+ LibHegel.check!(self, ctx, code)
368
+ out.read_pointer
369
+ end
370
+
371
+ # Registers a snapshot of +s+ as the profile +name+ for the whole
372
+ # process. The caller keeps ownership of +s+.
373
+ def settings_register_profile(ctx, name, s)
374
+ code = @hegel_settings_register_profile_fn.call(ctx, string_view(name), s)
375
+ LibHegel.check!(self, ctx, code)
376
+ nil
377
+ end
378
+
379
+ # "" clears an earlier call.
380
+ def set_default_profile(ctx, name)
381
+ code = @hegel_set_default_profile_fn.call(ctx, string_view(name))
382
+ LibHegel.check!(self, ctx, code)
383
+ nil
384
+ end
385
+
386
+ # Sets the Unicode version for the whole process. "" selects the
387
+ # engine's newest tables.
388
+ def set_unicode_version(ctx, version)
389
+ code = @hegel_set_unicode_version_fn.call(ctx, string_view(version))
390
+ LibHegel.check!(self, ctx, code)
391
+ nil
392
+ end
393
+
301
394
  # No-op when +s+ is nil, matching hegel_settings_free's documented
302
395
  # no-op-on-NULL contract; not translated, for the same reason as
303
396
  # #context_free: the header documents this call as always returning
@@ -358,6 +451,14 @@ module Hegel
358
451
  nil
359
452
  end
360
453
 
454
+ def settings_set_test_location(ctx, s, file, begin_line, class_name, function)
455
+ code = @hegel_settings_set_test_location_fn.call(
456
+ ctx, s, string_view(file), begin_line, string_view(class_name), string_view(function)
457
+ )
458
+ LibHegel.check!(self, ctx, code)
459
+ nil
460
+ end
461
+
361
462
  # +phases+ is a bitwise OR of the HEGEL_PHASE_* constants.
362
463
  def settings_set_phases(ctx, s, phases)
363
464
  code = @hegel_settings_set_phases_fn.call(ctx, s, phases)
@@ -373,6 +474,44 @@ module Hegel
373
474
  nil
374
475
  end
375
476
 
477
+ def settings_set_stop_on_first_failure(ctx, s, yes)
478
+ code = @hegel_settings_set_stop_on_first_failure_fn.call(ctx, s, yes)
479
+ LibHegel.check!(self, ctx, code)
480
+ nil
481
+ end
482
+
483
+ def settings_set_print_blob(ctx, s, yes)
484
+ code = @hegel_settings_set_print_blob_fn.call(ctx, s, yes)
485
+ LibHegel.check!(self, ctx, code)
486
+ nil
487
+ end
488
+
489
+ # libhegel never prints the blob; the caller decides from the resolved value.
490
+ def settings_get_print_blob(ctx, s)
491
+ out = FFI::MemoryPointer.new(:bool)
492
+ code = @hegel_settings_get_print_blob_fn.call(ctx, s, out)
493
+ LibHegel.check!(self, ctx, code)
494
+ out.read_uint8 != 0
495
+ end
496
+
497
+ def settings_set_show_statistics(ctx, s, yes)
498
+ code = @hegel_settings_set_show_statistics_fn.call(ctx, s, yes)
499
+ LibHegel.check!(self, ctx, code)
500
+ nil
501
+ end
502
+
503
+ def settings_set_nondeterminism_strictness(ctx, s, strictness)
504
+ code = @hegel_settings_set_nondeterminism_strictness_fn.call(ctx, s, strictness)
505
+ LibHegel.check!(self, ctx, code)
506
+ nil
507
+ end
508
+
509
+ def settings_set_backend(ctx, s, backend)
510
+ code = @hegel_settings_set_backend_fn.call(ctx, s, backend)
511
+ LibHegel.check!(self, ctx, code)
512
+ nil
513
+ end
514
+
376
515
  # +settings+ can be freed by the caller as soon as this call returns:
377
516
  # the header documents that hegel_run_start copies the settings it is
378
517
  # given rather than borrowing them. callback and user_data are always
@@ -461,6 +600,18 @@ module Hegel
461
600
  nil
462
601
  end
463
602
 
603
+ def event(ctx, tc, label)
604
+ code = @hegel_event_fn.call(ctx, tc, string_view(label))
605
+ LibHegel.check!(self, ctx, code)
606
+ nil
607
+ end
608
+
609
+ def event_value(ctx, tc, label, value)
610
+ code = @hegel_event_value_fn.call(ctx, tc, value, string_view(label))
611
+ LibHegel.check!(self, ctx, code)
612
+ nil
613
+ end
614
+
464
615
  # Returns a caller-owned copy of the finished run's result, or raises
465
616
  # HEGEL_E_NOT_COMPLETE (via LibHegel.check!) if the run has not
466
617
  # finished. The header documents this copy as staying valid after
@@ -631,7 +782,9 @@ module Hegel
631
782
  end
632
783
 
633
784
  # Closes the most recently opened span. +discard+ true marks it
634
- # rejected, so libhegel retries from before the span opened.
785
+ # rejected, so the shrinker can delete its draws. The engine does not
786
+ # draw again by itself: the caller does, as Hegel::Generator#filter
787
+ # does.
635
788
  def stop_span(ctx, tc, discard)
636
789
  code = @hegel_stop_span_fn.call(ctx, tc, discard)
637
790
  LibHegel.check!(self, ctx, code)
@@ -721,6 +874,16 @@ module Hegel
721
874
  out.read_int64
722
875
  end
723
876
 
877
+ # Puts back +variable_id+, which a consuming #pool_generate removed, so
878
+ # it can be drawn again. libhegel never does this by itself: a consumed
879
+ # id stays consumed until it is restored. An id that was not consumed
880
+ # raises Hegel::Error (HEGEL_E_INVALID_ARG).
881
+ def pool_restore(ctx, tc, pool, variable_id)
882
+ code = @hegel_pool_restore_fn.call(ctx, tc, pool, variable_id)
883
+ LibHegel.check!(self, ctx, code)
884
+ nil
885
+ end
886
+
724
887
  # No-op when +pool+ is nil, matching hegel_pool_free's documented
725
888
  # no-op-on-NULL contract; not translated, for the same reason as
726
889
  # #context_free.
@@ -731,6 +894,55 @@ module Hegel
731
894
  nil
732
895
  end
733
896
 
897
+ # Returns a caller-owned recursion handle, released separately with
898
+ # #recursion_free.
899
+ def new_recursion(ctx, tc, max_depth, max_leaves)
900
+ out = FFI::MemoryPointer.new(:pointer)
901
+ code = @hegel_new_recursion_fn.call(ctx, tc, max_depth, max_leaves, out)
902
+ LibHegel.check!(self, ctx, code)
903
+ out.read_pointer
904
+ end
905
+
906
+ # Returns true when the sub-value at +depth+ is a branch, false when
907
+ # it is a leaf.
908
+ def recursion_branch(ctx, tc, recursion, depth)
909
+ out = FFI::MemoryPointer.new(:bool)
910
+ code = @hegel_recursion_branch_fn.call(ctx, tc, recursion, depth, out)
911
+ LibHegel.check!(self, ctx, code)
912
+ out.read_uint8 != 0
913
+ end
914
+
915
+ # Returns false for HEGEL_E_RETRY: the attempt outgrew its leaf
916
+ # budget, and the caller unwinds it and calls #recursion_retry.
917
+ def recursion_leaf(ctx, tc, recursion)
918
+ code = @hegel_recursion_leaf_fn.call(ctx, tc, recursion)
919
+ retry_or_check!(ctx, code)
920
+ end
921
+
922
+ def recursion_retry(ctx, tc, recursion)
923
+ code = @hegel_recursion_retry_fn.call(ctx, tc, recursion)
924
+ LibHegel.check!(self, ctx, code)
925
+ nil
926
+ end
927
+
928
+ # Returns false for HEGEL_E_RETRY: the engine discarded the finished
929
+ # attempt, and the caller starts again from the root without calling
930
+ # #recursion_retry.
931
+ def recursion_finish(ctx, tc, recursion)
932
+ code = @hegel_recursion_finish_fn.call(ctx, tc, recursion)
933
+ retry_or_check!(ctx, code)
934
+ end
935
+
936
+ # No-op when +recursion+ is nil, matching hegel_recursion_free's
937
+ # documented no-op-on-NULL contract; not translated, for the same
938
+ # reason as #context_free.
939
+ def recursion_free(ctx, recursion)
940
+ # Removing this call only leaks native memory; Ruby has no way to
941
+ # observe the leak.
942
+ @hegel_recursion_free_fn.call(ctx, recursion) # mutineer:disable-line statement_removal
943
+ nil
944
+ end
945
+
734
946
  # Returns a caller-owned state-machine handle, released separately
735
947
  # with #state_machine_free. +rule_names+ and +invariant_names+ are
736
948
  # each an Array of Ruby Strings, packed into the hegel_string_t arrays
@@ -741,13 +953,16 @@ module Hegel
741
953
  # of labor #new_collection leaves to the caller for its own
742
954
  # min_size/max_size ordering.
743
955
  #
744
- # The machine is always sequential: one concurrency group, equal rule
745
- # weights, and a concurrency of exactly 1, so the engine draws no
746
- # concurrency level and the drawn level needs no reading. These
956
+ # +rule_weights+ holds one number per rule name; the engine rejects one
957
+ # that is not finite and positive.
958
+ #
959
+ # The machine is always sequential: one concurrency group and a
960
+ # concurrency of exactly 1, so the engine draws no concurrency level
961
+ # and the drawn level needs no reading. These
747
962
  # bindings drive the engine from one thread, and a concurrent machine
748
963
  # needs one worker thread per drawn level. For the same reason the
749
964
  # rule calls below pass worker index 0, the only worker there is.
750
- def new_state_machine(ctx, tc, rule_names, invariant_names, always_check, step_count)
965
+ def new_state_machine(ctx, tc, rule_names, rule_weights, invariant_names, always_check, step_count)
751
966
  out = FFI::MemoryPointer.new(:pointer)
752
967
  out_concurrency = FFI::MemoryPointer.new(:int64)
753
968
  # _rule_pointers / _invariant_pointers are unread past the call
@@ -762,9 +977,11 @@ module Hegel
762
977
  # A fresh MemoryPointer is zero-filled, which puts every rule in
763
978
  # group 0.
764
979
  rule_groups = FFI::MemoryPointer.new(:int64, rule_names.size)
980
+ weights = FFI::MemoryPointer.new(:double, rule_weights.size)
981
+ weights.write_array_of_double(rule_weights.map(&:to_f))
765
982
 
766
983
  code = @hegel_new_state_machine_fn.call(
767
- ctx, tc, rule_names_ptr, rule_groups, nil, rule_names.size,
984
+ ctx, tc, rule_names_ptr, rule_groups, weights, rule_names.size,
768
985
  invariant_names_ptr, pack_flags(always_check), invariant_names.size,
769
986
  1, 1, step_count, out, out_concurrency
770
987
  )
@@ -841,17 +1058,19 @@ module Hegel
841
1058
  # Returns a caller-owned string generator handle, released
842
1059
  # separately with #string_generator_free (or scoped with
843
1060
  # Hegel::TestCase#with_text_generator). +categories+ and
844
- # +exclude_categories+ are always passed as NULL/0 below, and
845
- # +include_characters+ and +exclude_characters+ as empty strings:
846
- # this call only wires up the codepoint-range constraints of the
847
- # 12-argument bind; the category and explicit-character filter
848
- # arguments are a later generator's scope, layered on top of this
849
- # same bind.
850
- def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF)
1061
+ # +exclude_categories+ are Arrays of Unicode general category names,
1062
+ # or nil for none: nil goes over as NULL, which is no restriction,
1063
+ # while an empty Array goes over as a non-NULL pointer with length
1064
+ # zero, which the header reads as an empty alphabet.
1065
+ def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF,
1066
+ categories: nil, exclude_categories: nil, include_characters: "", exclude_characters: "")
851
1067
  out = FFI::MemoryPointer.new(:pointer)
1068
+ category_array, category_slots = optional_name_array(categories)
1069
+ exclude_array, exclude_slots = optional_name_array(exclude_categories)
852
1070
  code = @hegel_string_generator_text_fn.call(
853
1071
  ctx, min_size, max_size, optional_string(codec), min_codepoint, max_codepoint,
854
- nil, 0, nil, 0, StringStruct.new, StringStruct.new,
1072
+ category_array, category_slots.size, exclude_array, exclude_slots.size,
1073
+ string_view(include_characters), string_view(exclude_characters),
855
1074
  out
856
1075
  )
857
1076
  LibHegel.check!(self, ctx, code)
@@ -1087,8 +1306,93 @@ module Hegel
1087
1306
  [read_date(out[:date]), read_time(out[:time])]
1088
1307
  end
1089
1308
 
1309
+ # Returns a caller-owned grammar builder for +text+, a grammar in
1310
+ # Lark's syntax, released with #grammar_builder_free.
1311
+ def grammar_builder_new(ctx, text)
1312
+ out = FFI::MemoryPointer.new(:pointer)
1313
+ code = @hegel_grammar_builder_new_fn.call(ctx, string_view(text.encode(Encoding::UTF_8)), out)
1314
+ LibHegel.check!(self, ctx, code)
1315
+ out.read_pointer
1316
+ end
1317
+
1318
+ # The path of the +index+th file the grammar imports, or nil past the
1319
+ # end of the list. HEGEL_E_NOT_FOUND marks that end rather than an
1320
+ # error, so it is answered before LibHegel.check! sees it.
1321
+ def grammar_builder_file(ctx, builder, index)
1322
+ out = StringStruct.new
1323
+ code = @hegel_grammar_builder_file_fn.call(ctx, builder, index, out)
1324
+ return nil if code == HEGEL_E_NOT_FOUND
1325
+
1326
+ LibHegel.check!(self, ctx, code)
1327
+ read_string(out)
1328
+ end
1329
+
1330
+ # Supplies the +index+th file's +text+, or nil for a file that does
1331
+ # not exist, which the header keeps apart from an empty file.
1332
+ def grammar_builder_provide(ctx, builder, index, text)
1333
+ code = @hegel_grammar_builder_provide_fn.call(ctx, builder, index, optional_string(text))
1334
+ LibHegel.check!(self, ctx, code)
1335
+ nil
1336
+ end
1337
+
1338
+ def grammar_builder_free(ctx, builder)
1339
+ # Removing this call only leaks native memory; Ruby has no way to
1340
+ # observe the leak.
1341
+ @hegel_grammar_builder_free_fn.call(ctx, builder) # mutineer:disable-line statement_removal
1342
+ nil
1343
+ end
1344
+
1345
+ # Returns a caller-owned grammar compiled from +builder+, released
1346
+ # with #grammar_free. +start+ nil is the rule `start`; +alphabet+ is
1347
+ # an optional text generator handle. +names+ always goes over as a
1348
+ # non-NULL array, as hegel-rust passes it, even when it is empty.
1349
+ def grammar_new(ctx, builder, start, max_size, max_depth, alphabet, names)
1350
+ out = FFI::MemoryPointer.new(:pointer)
1351
+ names_array, names_slots = optional_name_array(names)
1352
+ code = @hegel_grammar_new_fn.call(
1353
+ ctx, builder, optional_string(start), max_size, max_depth, alphabet, names_array, names_slots.size, out
1354
+ )
1355
+ LibHegel.check!(self, ctx, code)
1356
+ out.read_pointer
1357
+ end
1358
+
1359
+ # The marker standing for the explicit terminal +name+ in the strings
1360
+ # drawn from +grammar+, copied out of the grammar's own buffer.
1361
+ def grammar_placeholder(ctx, grammar, name)
1362
+ out = StringStruct.new
1363
+ code = @hegel_grammar_placeholder_fn.call(ctx, grammar, string_view(name), out)
1364
+ LibHegel.check!(self, ctx, code)
1365
+ read_string(out)
1366
+ end
1367
+
1368
+ def grammar_free(ctx, grammar)
1369
+ # Removing this call only leaks native memory; Ruby has no way to
1370
+ # observe the leak.
1371
+ @hegel_grammar_free_fn.call(ctx, grammar) # mutineer:disable-line statement_removal
1372
+ nil
1373
+ end
1374
+
1375
+ # A drawn string of +grammar+ of at most +max_size+ characters outside
1376
+ # its markers, 0 meaning the grammar's own cap. Freed like
1377
+ # #generate_string's result.
1378
+ def generate_grammar(ctx, tc, grammar, max_size)
1379
+ out = StringStruct.new
1380
+ code = @hegel_generate_grammar_fn.call(ctx, tc, grammar, max_size, out)
1381
+ LibHegel.check!(self, ctx, code)
1382
+ read_string(out)
1383
+ ensure
1384
+ owned_string_free(ctx, out)
1385
+ end
1386
+
1090
1387
  private
1091
1388
 
1389
+ def retry_or_check!(ctx, code)
1390
+ return false if code == HEGEL_E_RETRY
1391
+
1392
+ LibHegel.check!(self, ctx, code)
1393
+ true
1394
+ end
1395
+
1092
1396
  # Resolves +symbol+ against @handle and wraps it as a callable
1093
1397
  # FFI::Function, the direct form #initialize's own comment explains
1094
1398
  # the choice of.
@@ -1177,6 +1481,16 @@ module Hegel
1177
1481
  [array, slots]
1178
1482
  end
1179
1483
 
1484
+ # #pack_name_array for an argument where nil and an empty Array mean
1485
+ # different things: nil stays NULL, and an empty Array becomes a
1486
+ # non-NULL pointer, which #pack_name_array alone would turn into NULL.
1487
+ def optional_name_array(names)
1488
+ return [nil, []] unless names
1489
+
1490
+ array, slots = pack_name_array(names)
1491
+ [array || FFI::MemoryPointer.new(StringStruct), slots]
1492
+ end
1493
+
1180
1494
  # Packs +flags+ (an Array of true/false) into a const bool * buffer,
1181
1495
  # one byte each. An empty Array packs to a zero-length buffer, which the
1182
1496
  # engine reads zero flags from.
@@ -30,6 +30,8 @@ module Hegel
30
30
  HEGEL_E_NOT_COMPLETE = -7
31
31
  HEGEL_E_INTERNAL = -8
32
32
  HEGEL_E_CONCURRENT_USE = -9
33
+ HEGEL_E_RETRY = -10
34
+ HEGEL_E_NOT_FOUND = -11
33
35
 
34
36
  # The hegel.h name for each code above, so a translated error message
35
37
  # names the code instead of leaving the reader to cross-reference the
@@ -44,7 +46,9 @@ module Hegel
44
46
  HEGEL_E_ALREADY_COMPLETE => "HEGEL_E_ALREADY_COMPLETE",
45
47
  HEGEL_E_NOT_COMPLETE => "HEGEL_E_NOT_COMPLETE",
46
48
  HEGEL_E_INTERNAL => "HEGEL_E_INTERNAL",
47
- HEGEL_E_CONCURRENT_USE => "HEGEL_E_CONCURRENT_USE"
49
+ HEGEL_E_CONCURRENT_USE => "HEGEL_E_CONCURRENT_USE",
50
+ HEGEL_E_RETRY => "HEGEL_E_RETRY",
51
+ HEGEL_E_NOT_FOUND => "HEGEL_E_NOT_FOUND"
48
52
  }.freeze
49
53
 
50
54
  # hegel_status_t, named from hegel.h's enum of the same name. Passed to
@@ -67,6 +71,15 @@ module Hegel
67
71
  HEGEL_VERBOSITY_VERBOSE = 2
68
72
  HEGEL_VERBOSITY_DEBUG = 3
69
73
 
74
+ # hegel_nondeterminism_strictness_t
75
+ HEGEL_NONDETERMINISM_QUIET = 0
76
+ HEGEL_NONDETERMINISM_WARN = 1
77
+ HEGEL_NONDETERMINISM_ERROR = 2
78
+
79
+ # hegel_backend_t
80
+ HEGEL_BACKEND_DEFAULT = 1
81
+ HEGEL_BACKEND_URANDOM = 2
82
+
70
83
  # hegel_phase_t, named from hegel.h's enum of the same name. A bitwise
71
84
  # OR of these is passed to hegel_settings_set_phases; the default is
72
85
  # HEGEL_PHASE_ALL.
@@ -97,6 +110,14 @@ module Hegel
97
110
  # value a 64-bit unsigned integer maxes out at.
98
111
  HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED = (2**64) - 1
99
112
 
113
+ # HEGEL_MAX_SIZE_DEFAULT, named from hegel.h's #define of the same name,
114
+ # UINT64_MAX - 1. Since libhegel 0.49.0, hegel_new_collection,
115
+ # hegel_string_generator_text, and hegel_generate_bytes read this max_size
116
+ # as "no maximum given" and apply the default of text and bytes. The header
117
+ # tells a binding to pass UINT64_MAX for a caller's own maximum at or above
118
+ # it. See #max_size_arg.
119
+ HEGEL_MAX_SIZE_DEFAULT = (2**64) - 2
120
+
100
121
  # hegel_generate_float's smallest_nonzero_magnitude must be positive
101
122
  # and finite. The header names 5e-324 as the width-64 value that
102
123
  # places no restriction on which nonzero magnitudes get drawn.
@@ -108,7 +129,8 @@ module Hegel
108
129
  # responds to each name here.
109
130
  METHODS = %i[
110
131
  context_new context_free context_last_error version
111
- settings_new settings_free settings_set_test_cases settings_set_verbosity
132
+ settings_new settings_new_for_profile settings_register_profile set_default_profile set_unicode_version
133
+ settings_free settings_set_test_cases settings_set_verbosity
112
134
  settings_set_seed settings_set_derandomize settings_set_database
113
135
  run_start next_test_case run_free test_case_free mark_complete
114
136
  generate_boolean generate_integer generate_integer_big
@@ -124,13 +146,24 @@ module Hegel
124
146
  generate_ipv4 generate_ipv6 generate_uuid
125
147
  generate_date generate_time generate_datetime
126
148
  settings_set_phases settings_set_suppress_health_check settings_set_report_multiple_failures
127
- settings_set_database_key
128
- target
129
- new_pool pool_add pool_generate pool_free
149
+ settings_set_database_key settings_set_stop_on_first_failure settings_set_print_blob
150
+ settings_get_print_blob settings_set_show_statistics settings_set_nondeterminism_strictness
151
+ settings_set_backend settings_set_test_location
152
+ target event event_value
153
+ new_pool pool_add pool_generate pool_restore pool_free
130
154
  new_state_machine state_machine_next_group state_machine_next_rule state_machine_rule_rejected
131
155
  state_machine_should_check_invariant state_machine_free
156
+ grammar_builder_new grammar_builder_file grammar_builder_provide grammar_builder_free
157
+ grammar_new grammar_placeholder grammar_free generate_grammar
158
+ new_recursion recursion_branch recursion_leaf recursion_retry recursion_finish recursion_free
132
159
  ].freeze
133
160
 
161
+ # label_combine answers from this table: a recursive generator builds a
162
+ # fresh branch generator at every node it draws, and each one combines
163
+ # the same few labels again. Measured on the 30-leaf shrink in
164
+ # test_recursive_engine.rb, hashing the labels was a third of the run.
165
+ COMBINED_LABELS = {}
166
+
134
167
  module_function
135
168
 
136
169
  # Runs the block with a context obtained from +impl.context_new+,
@@ -187,6 +220,30 @@ module Hegel
187
220
  MESSAGE
188
221
  end
189
222
 
223
+ # What a freshly loaded engine needs before it builds a generator. Held
224
+ # here rather than in Real#initialize so the Fake can check it.
225
+ def prepare(impl, ctx, io: $stderr)
226
+ warn_on_version_mismatch(impl, ctx, io: io)
227
+ follow_host_unicode_version(impl, ctx)
228
+ end
229
+
230
+ # Sets the engine's Unicode version to the one Ruby's own Regexp and
231
+ # String methods follow, so a drawn character falls in the category Ruby
232
+ # puts it in (docs/adr/0036). The setting is process-wide and read when
233
+ # a generator is built, so it is made once, when the engine is loaded.
234
+ def follow_host_unicode_version(impl, ctx)
235
+ impl.set_unicode_version(ctx, RbConfig::CONFIG.fetch("UNICODE_VERSION"))
236
+ end
237
+
238
+ # The max_size to pass to the engine for a caller's own +max_size+. A
239
+ # maximum at or above HEGEL_MAX_SIZE_DEFAULT goes over as UINT64_MAX, no
240
+ # upper bound: otherwise the engine reads UINT64_MAX - 1 as its default of
241
+ # 100, and a larger value does not fit the u64 argument. No allocation can
242
+ # reach either size, so "no upper bound" keeps the caller's meaning.
243
+ def max_size_arg(max_size)
244
+ (max_size >= HEGEL_MAX_SIZE_DEFAULT) ? HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED : max_size
245
+ end
246
+
190
247
  # A span label is an opaque u64 that names the generator which opened
191
248
  # the span. The engine treats two spans with one label as draws of one
192
249
  # generator, and swaps or reorders them when it shrinks. hegel.h defines
@@ -200,7 +257,7 @@ module Hegel
200
257
  end
201
258
 
202
259
  def label_combine(labels)
203
- fnv1a(labels.pack("Q<*").bytes)
260
+ COMBINED_LABELS[labels] ||= fnv1a(labels.pack("Q<*").bytes)
204
261
  end
205
262
 
206
263
  def fnv1a(bytes)
@@ -5,5 +5,5 @@ module Hegel
5
5
  # Hegel::VERSION (the gem's own release): hegel-go and hegel-typescript keep
6
6
  # the two separate too, so bumping the pinned engine can land as its own
7
7
  # commit without also releasing a new gem version.
8
- LIBHEGEL_VERSION = "0.47.2"
8
+ LIBHEGEL_VERSION = "0.49.0"
9
9
  end