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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +221 -0
- data/README.ja.md +408 -0
- data/README.md +208 -102
- data/Rakefile +6 -3
- data/docs/README.md +11 -0
- data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
- data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
- data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
- data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
- data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
- data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
- data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
- data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
- data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
- data/docs/adr/0036-follow-rubys-unicode-version.md +50 -0
- data/docs/adr/0037-send-a-maximum-at-the-engine-default-sentinel-as-no-bound.md +48 -0
- data/docs/architecture.md +16 -1
- data/lib/hegel/errors.rb +24 -0
- data/lib/hegel/generator.rb +52 -25
- data/lib/hegel/generators.rb +715 -45
- data/lib/hegel/grammar_cache.rb +36 -0
- data/lib/hegel/lib_hegel/real.rb +333 -19
- data/lib/hegel/lib_hegel.rb +63 -6
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +13 -10
- data/lib/hegel/runner.rb +61 -27
- data/lib/hegel/settings.rb +92 -16
- data/lib/hegel/state_machine.rb +17 -4
- data/lib/hegel/stateful/pool.rb +52 -0
- data/lib/hegel/stateful.rb +2 -2
- data/lib/hegel/syntax/methods.rb +99 -15
- data/lib/hegel/test_case.rb +259 -37
- data/lib/hegel/test_location.rb +49 -0
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +21 -5
- data/sig/hegel.rbs +300 -42
- data/skills/hegel-ruby/references/ruby/reference.md +463 -41
- 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
|
data/lib/hegel/lib_hegel/real.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
#
|
|
745
|
-
#
|
|
746
|
-
#
|
|
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,
|
|
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
|
|
845
|
-
#
|
|
846
|
-
#
|
|
847
|
-
#
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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.
|
data/lib/hegel/lib_hegel.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
8
|
+
LIBHEGEL_VERSION = "0.49.0"
|
|
9
9
|
end
|