hegeltest 0.3.1-aarch64-linux → 0.4.0-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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +262 -0
  3. data/README.ja.md +408 -0
  4. data/README.md +209 -103
  5. data/Rakefile +6 -3
  6. data/docs/README.md +11 -0
  7. data/docs/adr/0025-copy-each-string-input-into-a-hegel-string-t.md +61 -0
  8. data/docs/adr/0026-select-the-engine-release-repository-by-version.md +47 -0
  9. data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
  10. data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
  11. data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
  12. data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
  13. data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
  14. data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
  15. data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
  16. data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
  17. data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
  18. data/docs/architecture.md +33 -11
  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 +455 -120
  24. data/lib/hegel/lib_hegel.rb +33 -8
  25. data/lib/hegel/libhegel/libhegel-linux-arm64.so +0 -0
  26. data/lib/hegel/libhegel_version.rb +1 -1
  27. data/lib/hegel/locate.rb +10 -10
  28. data/lib/hegel/report.rb +13 -10
  29. data/lib/hegel/runner.rb +61 -27
  30. data/lib/hegel/settings.rb +92 -16
  31. data/lib/hegel/state_machine.rb +17 -4
  32. data/lib/hegel/stateful/pool.rb +52 -0
  33. data/lib/hegel/stateful.rb +2 -2
  34. data/lib/hegel/syntax/methods.rb +99 -15
  35. data/lib/hegel/test_case.rb +256 -35
  36. data/lib/hegel/test_location.rb +49 -0
  37. data/lib/hegel/version.rb +1 -1
  38. data/lib/hegel.rb +21 -5
  39. data/lib/tasks/libhegel.rake +17 -5
  40. data/lib/tasks/platform_gems.rake +3 -3
  41. data/sig/hegel.rbs +296 -43
  42. data/skills/hegel-ruby/references/ruby/reference.md +465 -43
  43. metadata +15 -1
@@ -18,7 +18,7 @@ module Hegel
18
18
  # symbol.
19
19
  class Real
20
20
  # hegel_date_t / hegel_time_t / hegel_datetime_t, transcribed field
21
- # for field from hegel-c/include/hegel.h. FFI::Struct computes each
21
+ # for field from libhegel/include/hegel.h. FFI::Struct computes each
22
22
  # field's offset and the struct's own alignment padding from this
23
23
  # layout -- the same C-ABI rules the compiled library was built
24
24
  # against -- so there is no hand-computed byte offset here to drift
@@ -40,17 +40,19 @@ module Hegel
40
40
  layout :date, DateStruct, :time, TimeStruct
41
41
  end
42
42
 
43
- # hegel_generate_string_result_t and hegel_generate_bytes_result_t are
44
- # both `{ char *data; size_t len; }` in hegel-c/include/hegel.h --
45
- # byte-for-byte the same layout under two names, one per element type
46
- # the two calls draw. One Ruby struct mirrors both; #generate_string
47
- # and #generate_bytes each pass a fresh instance as the out-parameter
48
- # and read it back through their own method below.
49
- class RawResultStruct < FFI::Struct
43
+ # hegel_string_t (`{ const char *data; size_t len; }`), and
44
+ # hegel_generate_bytes_result_t (`{ uint8_t *data; size_t len; }`),
45
+ # from libhegel/include/hegel.h: the same layout under two names. Since
46
+ # libhegel 0.46 every string crosses the ABI as a hegel_string_t: by
47
+ # value for an ordinary input, through a nullable pointer for an
48
+ # optional input, and as a struct the engine writes for an output.
49
+ # hegel_owned_string_t, which #generate_string reads, is a typedef of
50
+ # hegel_string_t. One Ruby struct mirrors all of them.
51
+ class StringStruct < FFI::Struct
50
52
  layout :data, :pointer, :len, :size_t
51
53
  end
52
54
 
53
- private_constant :DateStruct, :TimeStruct, :DatetimeStruct, :RawResultStruct
55
+ private_constant :DateStruct, :TimeStruct, :DatetimeStruct, :StringStruct
54
56
 
55
57
  # Opens +path+ (default: Hegel::Locate.resolve) and binds the
56
58
  # functions this boundary calls. Immediately after, opens a context
@@ -75,11 +77,18 @@ module Hegel
75
77
 
76
78
  @hegel_context_new_fn = bind("hegel_context_new", [], :pointer)
77
79
  @hegel_context_free_fn = bind("hegel_context_free", [:pointer], :int32)
78
- @hegel_context_last_error_fn = bind("hegel_context_last_error", [:pointer], :pointer)
80
+ @hegel_context_last_error_fn = bind("hegel_context_last_error", [:pointer], StringStruct.by_value)
79
81
  @hegel_version_fn = bind("hegel_version", [:pointer, :pointer], :int32)
80
82
 
81
83
  @hegel_settings_new_fn = bind("hegel_settings_new", [:pointer, :pointer], :int32)
82
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)
83
92
  @hegel_settings_set_test_cases_fn = bind(
84
93
  "hegel_settings_set_test_cases", [:pointer, :pointer, :uint64], :int32
85
94
  )
@@ -93,13 +102,13 @@ module Hegel
93
102
  "hegel_settings_set_derandomize", [:pointer, :pointer, :bool], :int32
94
103
  )
95
104
  @hegel_settings_set_database_fn = bind(
96
- "hegel_settings_set_database", [:pointer, :pointer, :string], :int32
105
+ "hegel_settings_set_database", [:pointer, :pointer, :pointer], :int32
97
106
  )
98
107
  @hegel_settings_set_report_multiple_failures_fn = bind(
99
108
  "hegel_settings_set_report_multiple_failures", [:pointer, :pointer, :bool], :int32
100
109
  )
101
110
  @hegel_settings_set_database_key_fn = bind(
102
- "hegel_settings_set_database_key", [:pointer, :pointer, :string], :int32
111
+ "hegel_settings_set_database_key", [:pointer, :pointer, :pointer], :int32
103
112
  )
104
113
  @hegel_settings_set_phases_fn = bind(
105
114
  "hegel_settings_set_phases", [:pointer, :pointer, :uint32], :int32
@@ -107,12 +116,30 @@ module Hegel
107
116
  @hegel_settings_set_suppress_health_check_fn = bind(
108
117
  "hegel_settings_set_suppress_health_check", [:pointer, :pointer, :uint32], :int32
109
118
  )
119
+ @hegel_settings_set_stop_on_first_failure_fn = bind(
120
+ "hegel_settings_set_stop_on_first_failure", [:pointer, :pointer, :bool], :int32
121
+ )
122
+ @hegel_settings_set_print_blob_fn = bind("hegel_settings_set_print_blob", [:pointer, :pointer, :bool], :int32)
123
+ @hegel_settings_get_print_blob_fn = bind(
124
+ "hegel_settings_get_print_blob", [:pointer, :pointer, :pointer], :int32
125
+ )
126
+ @hegel_settings_set_show_statistics_fn = bind(
127
+ "hegel_settings_set_show_statistics", [:pointer, :pointer, :bool], :int32
128
+ )
129
+ @hegel_settings_set_nondeterminism_strictness_fn = bind(
130
+ "hegel_settings_set_nondeterminism_strictness", [:pointer, :pointer, :uint32], :int32
131
+ )
132
+ @hegel_settings_set_backend_fn = bind("hegel_settings_set_backend", [:pointer, :pointer, :uint32], :int32)
133
+ @hegel_settings_set_test_location_fn = bind(
134
+ "hegel_settings_set_test_location",
135
+ [:pointer, :pointer, StringStruct.by_value, :uint32, StringStruct.by_value, StringStruct.by_value], :int32
136
+ )
110
137
 
111
138
  @hegel_run_start_fn = bind(
112
139
  "hegel_run_start", [:pointer, :pointer, :pointer, :pointer, :pointer], :int32
113
140
  )
114
141
  @hegel_run_start_blob_fn = bind(
115
- "hegel_run_start_blob", [:pointer, :pointer, :string, :pointer, :pointer, :pointer], :int32
142
+ "hegel_run_start_blob", [:pointer, :pointer, StringStruct.by_value, :pointer, :pointer, :pointer], :int32
116
143
  )
117
144
  @hegel_next_test_case_fn = bind("hegel_next_test_case", [:pointer, :pointer, :pointer], :int32)
118
145
  @hegel_test_case_should_capture_fn = bind(
@@ -120,8 +147,12 @@ module Hegel
120
147
  )
121
148
  @hegel_run_free_fn = bind("hegel_run_free", [:pointer, :pointer], :int32)
122
149
  @hegel_test_case_free_fn = bind("hegel_test_case_free", [:pointer, :pointer], :int32)
123
- @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :string], :int32)
124
- @hegel_target_fn = bind("hegel_target", [:pointer, :pointer, :double, :string], :int32)
150
+ @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :pointer], :int32)
151
+ @hegel_target_fn = bind("hegel_target", [:pointer, :pointer, :double, StringStruct.by_value], :int32)
152
+ @hegel_event_fn = bind("hegel_event", [:pointer, :pointer, StringStruct.by_value], :int32)
153
+ @hegel_event_value_fn = bind(
154
+ "hegel_event_value", [:pointer, :pointer, :double, StringStruct.by_value], :int32
155
+ )
125
156
 
126
157
  @hegel_run_result_fn = bind("hegel_run_result", [:pointer, :pointer, :pointer], :int32)
127
158
  @hegel_run_result_free_fn = bind("hegel_run_result_free", [:pointer, :pointer], :int32)
@@ -159,7 +190,7 @@ module Hegel
159
190
  )
160
191
  @hegel_collection_more_fn = bind("hegel_collection_more", [:pointer, :pointer, :pointer, :pointer], :int32)
161
192
  @hegel_collection_reject_fn = bind(
162
- "hegel_collection_reject", [:pointer, :pointer, :pointer, :string], :int32
193
+ "hegel_collection_reject", [:pointer, :pointer, :pointer, :pointer], :int32
163
194
  )
164
195
  @hegel_collection_free_fn = bind("hegel_collection_free", [:pointer, :pointer], :int32)
165
196
 
@@ -168,8 +199,20 @@ module Hegel
168
199
  @hegel_pool_generate_fn = bind(
169
200
  "hegel_pool_generate", [:pointer, :pointer, :pointer, :bool, :pointer], :int32
170
201
  )
202
+ @hegel_pool_restore_fn = bind("hegel_pool_restore", [:pointer, :pointer, :pointer, :int64], :int32)
171
203
  @hegel_pool_free_fn = bind("hegel_pool_free", [:pointer, :pointer], :int32)
172
204
 
205
+ @hegel_new_recursion_fn = bind(
206
+ "hegel_new_recursion", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
207
+ )
208
+ @hegel_recursion_branch_fn = bind(
209
+ "hegel_recursion_branch", [:pointer, :pointer, :pointer, :uint64, :pointer], :int32
210
+ )
211
+ @hegel_recursion_leaf_fn = bind("hegel_recursion_leaf", [:pointer, :pointer, :pointer], :int32)
212
+ @hegel_recursion_retry_fn = bind("hegel_recursion_retry", [:pointer, :pointer, :pointer], :int32)
213
+ @hegel_recursion_finish_fn = bind("hegel_recursion_finish", [:pointer, :pointer, :pointer], :int32)
214
+ @hegel_recursion_free_fn = bind("hegel_recursion_free", [:pointer, :pointer], :int32)
215
+
173
216
  @hegel_new_state_machine_fn = bind(
174
217
  "hegel_new_state_machine",
175
218
  [
@@ -196,14 +239,10 @@ module Hegel
196
239
  [:pointer, :pointer, :uint32, :double, :double, :bool, :bool, :bool, :bool, :double, :pointer], :int32
197
240
  )
198
241
 
199
- # hegel_string_generator_text takes 14 arguments past ctx; the
200
- # last 8 (categories_len through exclude_characters_len) are the
201
- # category and explicit-character filter parameters this task
202
- # does not wire up (see #string_generator_text).
203
242
  @hegel_string_generator_text_fn = bind(
204
243
  "hegel_string_generator_text",
205
- [:pointer, :uint64, :uint64, :string, :uint32, :uint32,
206
- :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t,
244
+ [:pointer, :uint64, :uint64, :pointer, :uint32, :uint32,
245
+ :pointer, :size_t, :pointer, :size_t, StringStruct.by_value, StringStruct.by_value,
207
246
  :pointer],
208
247
  :int32
209
248
  )
@@ -211,9 +250,7 @@ module Hegel
211
250
  @hegel_generate_string_fn = bind(
212
251
  "hegel_generate_string", [:pointer, :pointer, :pointer, :pointer], :int32
213
252
  )
214
- @hegel_generate_string_result_free_fn = bind(
215
- "hegel_generate_string_result_free", [:pointer, :pointer], :int32
216
- )
253
+ @hegel_owned_string_free_fn = bind("hegel_owned_string_free", [:pointer, :pointer], :int32)
217
254
 
218
255
  @hegel_generate_bytes_fn = bind(
219
256
  "hegel_generate_bytes", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
@@ -223,7 +260,7 @@ module Hegel
223
260
  )
224
261
 
225
262
  @hegel_string_generator_regex_fn = bind(
226
- "hegel_string_generator_regex", [:pointer, :pointer, :size_t, :bool, :pointer, :pointer], :int32
263
+ "hegel_string_generator_regex", [:pointer, StringStruct.by_value, :bool, :pointer, :pointer], :int32
227
264
  )
228
265
  @hegel_string_generator_email_fn = bind("hegel_string_generator_email", [:pointer, :pointer], :int32)
229
266
  @hegel_string_generator_url_fn = bind("hegel_string_generator_url", [:pointer, :pointer], :int32)
@@ -248,6 +285,28 @@ module Hegel
248
285
  [:pointer, :pointer, DatetimeStruct.by_value, DatetimeStruct.by_value, :pointer], :int32
249
286
  )
250
287
 
288
+ @hegel_grammar_builder_new_fn = bind(
289
+ "hegel_grammar_builder_new", [:pointer, StringStruct.by_value, :pointer], :int32
290
+ )
291
+ @hegel_grammar_builder_file_fn = bind(
292
+ "hegel_grammar_builder_file", [:pointer, :pointer, :size_t, :pointer], :int32
293
+ )
294
+ @hegel_grammar_builder_provide_fn = bind(
295
+ "hegel_grammar_builder_provide", [:pointer, :pointer, :size_t, :pointer], :int32
296
+ )
297
+ @hegel_grammar_builder_free_fn = bind("hegel_grammar_builder_free", [:pointer, :pointer], :int32)
298
+ @hegel_grammar_new_fn = bind(
299
+ "hegel_grammar_new",
300
+ [:pointer, :pointer, :pointer, :uint64, :uint32, :pointer, :pointer, :size_t, :pointer], :int32
301
+ )
302
+ @hegel_grammar_placeholder_fn = bind(
303
+ "hegel_grammar_placeholder", [:pointer, :pointer, StringStruct.by_value, :pointer], :int32
304
+ )
305
+ @hegel_grammar_free_fn = bind("hegel_grammar_free", [:pointer, :pointer], :int32)
306
+ @hegel_generate_grammar_fn = bind(
307
+ "hegel_generate_grammar", [:pointer, :pointer, :pointer, :uint64, :pointer], :int32
308
+ )
309
+
251
310
  LibHegel.with_context(self) { |ctx| LibHegel.warn_on_version_mismatch(self, ctx, io: io) }
252
311
  end
253
312
 
@@ -273,18 +332,18 @@ module Hegel
273
332
  # before returning, since the header documents that buffer as
274
333
  # borrowed and invalidated by the next call taking the same context.
275
334
  def context_last_error(ctx)
276
- utf8(@hegel_context_last_error_fn.call(ctx))
335
+ read_string(@hegel_context_last_error_fn.call(ctx))
277
336
  end
278
337
 
279
338
  # Returns the loaded engine's version string, or raises the
280
339
  # exception LibHegel.check! translates this call's result code to.
281
340
  def version(ctx)
282
- out = FFI::MemoryPointer.new(:pointer)
341
+ out = StringStruct.new
283
342
  code = @hegel_version_fn.call(ctx, out)
284
343
  # This call takes only ctx and this method's own out argument; neither
285
344
  # makes the real engine return anything but HEGEL_OK.
286
345
  LibHegel.check!(self, ctx, code) # mutineer:disable-line statement_removal
287
- utf8(out.read_pointer)
346
+ read_string(out)
288
347
  end
289
348
 
290
349
  # Returns a settings handle initialized from the engine's resolved
@@ -298,6 +357,31 @@ module Hegel
298
357
  out.read_pointer
299
358
  end
300
359
 
360
+ # Like #settings_new, but starts from the profile +name+, which still
361
+ # extends the default profile (except "base"). An unknown name returns
362
+ # HEGEL_E_INVALID_ARG.
363
+ def settings_new_for_profile(ctx, name)
364
+ out = FFI::MemoryPointer.new(:pointer)
365
+ code = @hegel_settings_new_for_profile_fn.call(ctx, string_view(name), out)
366
+ LibHegel.check!(self, ctx, code)
367
+ out.read_pointer
368
+ end
369
+
370
+ # Registers a snapshot of +s+ as the profile +name+ for the whole
371
+ # process. The caller keeps ownership of +s+.
372
+ def settings_register_profile(ctx, name, s)
373
+ code = @hegel_settings_register_profile_fn.call(ctx, string_view(name), s)
374
+ LibHegel.check!(self, ctx, code)
375
+ nil
376
+ end
377
+
378
+ # "" clears an earlier call.
379
+ def set_default_profile(ctx, name)
380
+ code = @hegel_set_default_profile_fn.call(ctx, string_view(name))
381
+ LibHegel.check!(self, ctx, code)
382
+ nil
383
+ end
384
+
301
385
  # No-op when +s+ is nil, matching hegel_settings_free's documented
302
386
  # no-op-on-NULL contract; not translated, for the same reason as
303
387
  # #context_free: the header documents this call as always returning
@@ -334,11 +418,11 @@ module Hegel
334
418
  end
335
419
 
336
420
  # +database+ may be nil (libhegel's own default path) or a String,
337
- # including "" to disable the database. Declared :string below, so a
338
- # Ruby String marshals as a const char* to its bytes and nil marshals
339
- # to NULL, both directly -- no separate pointer to build here.
421
+ # including "" to disable the database. The header takes an optional
422
+ # string here, so nil goes over as a NULL pointer and "" as a pointer
423
+ # to an empty string; see #optional_string.
340
424
  def settings_set_database(ctx, s, database)
341
- code = @hegel_settings_set_database_fn.call(ctx, s, database)
425
+ code = @hegel_settings_set_database_fn.call(ctx, s, optional_string(database))
342
426
  LibHegel.check!(self, ctx, code)
343
427
  nil
344
428
  end
@@ -350,10 +434,18 @@ module Hegel
350
434
  end
351
435
 
352
436
  # +key+ may be nil, which the header documents as clearing the key
353
- # (the default); nil marshals to NULL for this :string argument, the
437
+ # (the default); nil goes over as NULL through #optional_string, the
354
438
  # same as #settings_set_database's own nilable database argument.
355
439
  def settings_set_database_key(ctx, s, key)
356
- code = @hegel_settings_set_database_key_fn.call(ctx, s, key)
440
+ code = @hegel_settings_set_database_key_fn.call(ctx, s, optional_string(key))
441
+ LibHegel.check!(self, ctx, code)
442
+ nil
443
+ end
444
+
445
+ def settings_set_test_location(ctx, s, file, begin_line, class_name, function)
446
+ code = @hegel_settings_set_test_location_fn.call(
447
+ ctx, s, string_view(file), begin_line, string_view(class_name), string_view(function)
448
+ )
357
449
  LibHegel.check!(self, ctx, code)
358
450
  nil
359
451
  end
@@ -373,6 +465,44 @@ module Hegel
373
465
  nil
374
466
  end
375
467
 
468
+ def settings_set_stop_on_first_failure(ctx, s, yes)
469
+ code = @hegel_settings_set_stop_on_first_failure_fn.call(ctx, s, yes)
470
+ LibHegel.check!(self, ctx, code)
471
+ nil
472
+ end
473
+
474
+ def settings_set_print_blob(ctx, s, yes)
475
+ code = @hegel_settings_set_print_blob_fn.call(ctx, s, yes)
476
+ LibHegel.check!(self, ctx, code)
477
+ nil
478
+ end
479
+
480
+ # libhegel never prints the blob; the caller decides from the resolved value.
481
+ def settings_get_print_blob(ctx, s)
482
+ out = FFI::MemoryPointer.new(:bool)
483
+ code = @hegel_settings_get_print_blob_fn.call(ctx, s, out)
484
+ LibHegel.check!(self, ctx, code)
485
+ out.read_uint8 != 0
486
+ end
487
+
488
+ def settings_set_show_statistics(ctx, s, yes)
489
+ code = @hegel_settings_set_show_statistics_fn.call(ctx, s, yes)
490
+ LibHegel.check!(self, ctx, code)
491
+ nil
492
+ end
493
+
494
+ def settings_set_nondeterminism_strictness(ctx, s, strictness)
495
+ code = @hegel_settings_set_nondeterminism_strictness_fn.call(ctx, s, strictness)
496
+ LibHegel.check!(self, ctx, code)
497
+ nil
498
+ end
499
+
500
+ def settings_set_backend(ctx, s, backend)
501
+ code = @hegel_settings_set_backend_fn.call(ctx, s, backend)
502
+ LibHegel.check!(self, ctx, code)
503
+ nil
504
+ end
505
+
376
506
  # +settings+ can be freed by the caller as soon as this call returns:
377
507
  # the header documents that hegel_run_start copies the settings it is
378
508
  # given rather than borrowing them. callback and user_data are always
@@ -393,7 +523,7 @@ module Hegel
393
523
  # version.
394
524
  def run_start_blob(ctx, settings, blob)
395
525
  out = FFI::MemoryPointer.new(:pointer)
396
- code = @hegel_run_start_blob_fn.call(ctx, settings, blob, nil, nil, out)
526
+ code = @hegel_run_start_blob_fn.call(ctx, settings, string_view(blob), nil, nil, out)
397
527
  LibHegel.check!(self, ctx, code)
398
528
  out.read_pointer
399
529
  end
@@ -442,9 +572,9 @@ module Hegel
442
572
  # +origin+ must be non-nil only when +status+ is
443
573
  # HEGEL_STATUS_INTERESTING, per the header; this layer neither builds
444
574
  # nor validates that string, only passes through what the caller
445
- # supplies. nil marshals to NULL for this :string argument.
575
+ # supplies. nil goes over as NULL through #optional_string.
446
576
  def mark_complete(ctx, tc, status, origin)
447
- code = @hegel_mark_complete_fn.call(ctx, tc, status, origin)
577
+ code = @hegel_mark_complete_fn.call(ctx, tc, status, optional_string(origin))
448
578
  LibHegel.check!(self, ctx, code)
449
579
  nil
450
580
  end
@@ -456,7 +586,19 @@ module Hegel
456
586
  # checked here, matching how this layer leaves every other
457
587
  # argument-shape rule to the engine's own HEGEL_E_INVALID_ARG.
458
588
  def target(ctx, tc, value, label)
459
- code = @hegel_target_fn.call(ctx, tc, value, label)
589
+ code = @hegel_target_fn.call(ctx, tc, value, string_view(label))
590
+ LibHegel.check!(self, ctx, code)
591
+ nil
592
+ end
593
+
594
+ def event(ctx, tc, label)
595
+ code = @hegel_event_fn.call(ctx, tc, string_view(label))
596
+ LibHegel.check!(self, ctx, code)
597
+ nil
598
+ end
599
+
600
+ def event_value(ctx, tc, label, value)
601
+ code = @hegel_event_value_fn.call(ctx, tc, value, string_view(label))
460
602
  LibHegel.check!(self, ctx, code)
461
603
  nil
462
604
  end
@@ -538,10 +680,10 @@ module Hegel
538
680
  # returning, since it is owned by the failure and only valid until
539
681
  # #failure_free.
540
682
  def failure_origin(ctx, f)
541
- out = FFI::MemoryPointer.new(:pointer)
683
+ out = StringStruct.new
542
684
  code = @hegel_failure_origin_fn.call(ctx, f, out)
543
685
  LibHegel.check!(self, ctx, code)
544
- utf8(out.read_pointer)
686
+ read_string(out)
545
687
  end
546
688
 
547
689
  # Returns nil when libhegel produced no reproduction blob for this
@@ -659,12 +801,12 @@ module Hegel
659
801
  end
660
802
 
661
803
  # Tells libhegel the last element +collection+ produced is invalid.
662
- # +why+ is an optional human-readable reason (nil marshals to NULL
663
- # for this :string argument, which the header allows); the header
804
+ # +why+ is an optional human-readable reason (nil goes over as NULL
805
+ # through #optional_string, which the header allows); the header
664
806
  # documents it as validated but reserved for future rejection
665
807
  # diagnostics, unused today.
666
808
  def collection_reject(ctx, tc, collection, why = nil)
667
- code = @hegel_collection_reject_fn.call(ctx, tc, collection, why)
809
+ code = @hegel_collection_reject_fn.call(ctx, tc, collection, optional_string(why))
668
810
  LibHegel.check!(self, ctx, code)
669
811
  nil
670
812
  end
@@ -721,6 +863,16 @@ module Hegel
721
863
  out.read_int64
722
864
  end
723
865
 
866
+ # Puts back +variable_id+, which a consuming #pool_generate removed, so
867
+ # it can be drawn again. libhegel never does this by itself: a consumed
868
+ # id stays consumed until it is restored. An id that was not consumed
869
+ # raises Hegel::Error (HEGEL_E_INVALID_ARG).
870
+ def pool_restore(ctx, tc, pool, variable_id)
871
+ code = @hegel_pool_restore_fn.call(ctx, tc, pool, variable_id)
872
+ LibHegel.check!(self, ctx, code)
873
+ nil
874
+ end
875
+
724
876
  # No-op when +pool+ is nil, matching hegel_pool_free's documented
725
877
  # no-op-on-NULL contract; not translated, for the same reason as
726
878
  # #context_free.
@@ -731,23 +883,75 @@ module Hegel
731
883
  nil
732
884
  end
733
885
 
886
+ # Returns a caller-owned recursion handle, released separately with
887
+ # #recursion_free.
888
+ def new_recursion(ctx, tc, max_depth, max_leaves)
889
+ out = FFI::MemoryPointer.new(:pointer)
890
+ code = @hegel_new_recursion_fn.call(ctx, tc, max_depth, max_leaves, out)
891
+ LibHegel.check!(self, ctx, code)
892
+ out.read_pointer
893
+ end
894
+
895
+ # Returns true when the sub-value at +depth+ is a branch, false when
896
+ # it is a leaf.
897
+ def recursion_branch(ctx, tc, recursion, depth)
898
+ out = FFI::MemoryPointer.new(:bool)
899
+ code = @hegel_recursion_branch_fn.call(ctx, tc, recursion, depth, out)
900
+ LibHegel.check!(self, ctx, code)
901
+ out.read_uint8 != 0
902
+ end
903
+
904
+ # Returns false for HEGEL_E_RETRY: the attempt outgrew its leaf
905
+ # budget, and the caller unwinds it and calls #recursion_retry.
906
+ def recursion_leaf(ctx, tc, recursion)
907
+ code = @hegel_recursion_leaf_fn.call(ctx, tc, recursion)
908
+ retry_or_check!(ctx, code)
909
+ end
910
+
911
+ def recursion_retry(ctx, tc, recursion)
912
+ code = @hegel_recursion_retry_fn.call(ctx, tc, recursion)
913
+ LibHegel.check!(self, ctx, code)
914
+ nil
915
+ end
916
+
917
+ # Returns false for HEGEL_E_RETRY: the engine discarded the finished
918
+ # attempt, and the caller starts again from the root without calling
919
+ # #recursion_retry.
920
+ def recursion_finish(ctx, tc, recursion)
921
+ code = @hegel_recursion_finish_fn.call(ctx, tc, recursion)
922
+ retry_or_check!(ctx, code)
923
+ end
924
+
925
+ # No-op when +recursion+ is nil, matching hegel_recursion_free's
926
+ # documented no-op-on-NULL contract; not translated, for the same
927
+ # reason as #context_free.
928
+ def recursion_free(ctx, recursion)
929
+ # Removing this call only leaks native memory; Ruby has no way to
930
+ # observe the leak.
931
+ @hegel_recursion_free_fn.call(ctx, recursion) # mutineer:disable-line statement_removal
932
+ nil
933
+ end
934
+
734
935
  # Returns a caller-owned state-machine handle, released separately
735
936
  # with #state_machine_free. +rule_names+ and +invariant_names+ are
736
- # each an Array of Ruby Strings, packed into the const char *const *
737
- # arguments hegel_new_state_machine expects by #pack_name_array; see
738
- # that method's own comment for how and why. +always_check+ holds one
937
+ # each an Array of Ruby Strings, packed into the hegel_string_t arrays
938
+ # hegel_new_state_machine expects by #pack_name_array; see that
939
+ # method's own comment for how and why. +always_check+ holds one
739
940
  # flag per invariant name. Validating +rule_names+ as non-empty (the
740
941
  # header's own requirement) is left to the caller, the same division
741
942
  # of labor #new_collection leaves to the caller for its own
742
943
  # min_size/max_size ordering.
743
944
  #
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
945
+ # +rule_weights+ holds one number per rule name; the engine rejects one
946
+ # that is not finite and positive.
947
+ #
948
+ # The machine is always sequential: one concurrency group and a
949
+ # concurrency of exactly 1, so the engine draws no concurrency level
950
+ # and the drawn level needs no reading. These
747
951
  # bindings drive the engine from one thread, and a concurrent machine
748
952
  # needs one worker thread per drawn level. For the same reason the
749
953
  # 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)
954
+ def new_state_machine(ctx, tc, rule_names, rule_weights, invariant_names, always_check, step_count)
751
955
  out = FFI::MemoryPointer.new(:pointer)
752
956
  out_concurrency = FFI::MemoryPointer.new(:int64)
753
957
  # _rule_pointers / _invariant_pointers are unread past the call
@@ -762,9 +966,11 @@ module Hegel
762
966
  # A fresh MemoryPointer is zero-filled, which puts every rule in
763
967
  # group 0.
764
968
  rule_groups = FFI::MemoryPointer.new(:int64, rule_names.size)
969
+ weights = FFI::MemoryPointer.new(:double, rule_weights.size)
970
+ weights.write_array_of_double(rule_weights.map(&:to_f))
765
971
 
766
972
  code = @hegel_new_state_machine_fn.call(
767
- ctx, tc, rule_names_ptr, rule_groups, nil, rule_names.size,
973
+ ctx, tc, rule_names_ptr, rule_groups, weights, rule_names.size,
768
974
  invariant_names_ptr, pack_flags(always_check), invariant_names.size,
769
975
  1, 1, step_count, out, out_concurrency
770
976
  )
@@ -840,17 +1046,20 @@ module Hegel
840
1046
 
841
1047
  # Returns a caller-owned string generator handle, released
842
1048
  # separately with #string_generator_free (or scoped with
843
- # Hegel::TestCase#with_text_generator). +categories+,
844
- # +exclude_categories+, +include_characters+, and
845
- # +exclude_characters+ are always passed as NULL/0 below: this call
846
- # only wires up the codepoint-range constraints of the 14-argument
847
- # bind; the category and explicit-character filter arguments are a
848
- # later generator's scope, layered on top of this same bind.
849
- def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF)
1049
+ # Hegel::TestCase#with_text_generator). +categories+ and
1050
+ # +exclude_categories+ are Arrays of Unicode general category names,
1051
+ # or nil for none: nil goes over as NULL, which is no restriction,
1052
+ # while an empty Array goes over as a non-NULL pointer with length
1053
+ # zero, which the header reads as an empty alphabet.
1054
+ def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF,
1055
+ categories: nil, exclude_categories: nil, include_characters: "", exclude_characters: "")
850
1056
  out = FFI::MemoryPointer.new(:pointer)
1057
+ category_array, category_slots = optional_name_array(categories)
1058
+ exclude_array, exclude_slots = optional_name_array(exclude_categories)
851
1059
  code = @hegel_string_generator_text_fn.call(
852
- ctx, min_size, max_size, codec, min_codepoint, max_codepoint,
853
- nil, 0, nil, 0, nil, 0, nil, 0,
1060
+ ctx, min_size, max_size, optional_string(codec), min_codepoint, max_codepoint,
1061
+ category_array, category_slots.size, exclude_array, exclude_slots.size,
1062
+ string_view(include_characters), string_view(exclude_characters),
854
1063
  out
855
1064
  )
856
1065
  LibHegel.check!(self, ctx, code)
@@ -867,47 +1076,46 @@ module Hegel
867
1076
  nil
868
1077
  end
869
1078
 
870
- # Returns a drawn String, force-encoded as UTF-8 (this codec's
871
- # alphabet), copying exactly the returned +len+ bytes rather than
872
- # reading a NUL-terminated buffer: the header documents it as not
873
- # NUL-terminated and possibly containing interior NUL bytes, since
874
- # the drawn alphabet can include U+0000.
1079
+ # Returns a drawn String, labelled UTF-8 by #read_string. The draw can
1080
+ # hold interior NUL bytes, since the drawn alphabet can include
1081
+ # U+0000, so #read_string copies exactly the returned +len+ bytes.
875
1082
  #
876
1083
  # The native buffer is released in +ensure+ via
877
- # #generate_string_result_free, called directly from here rather
1084
+ # #owned_string_free, called directly from here rather
878
1085
  # than left to the caller: nothing outside this file may hold or
879
1086
  # read the raw struct (ffi is confined to this file), so unlike
880
1087
  # #new_collection or #string_generator_text, the freeable handle
881
1088
  # here never leaves this method. Freeing is safe even when the draw
882
1089
  # above raised: +out+ starts zero-filled (FFI::Struct.new allocates
883
1090
  # cleared memory) and libhegel only writes into it on success, so
884
- # the struct #generate_string_result_free sees here is always
1091
+ # the struct #owned_string_free sees here is always
885
1092
  # either a completed draw or the all-zero state the header
886
1093
  # documents as already safe to free.
887
1094
  def generate_string(ctx, tc, generator)
888
- out = RawResultStruct.new
1095
+ out = StringStruct.new
889
1096
  code = @hegel_generate_string_fn.call(ctx, tc, generator, out)
890
1097
  LibHegel.check!(self, ctx, code)
891
- out[:data].read_bytes(out[:len]).force_encoding(Encoding::UTF_8)
1098
+ read_string(out)
892
1099
  ensure
893
- generate_string_result_free(ctx, out)
1100
+ owned_string_free(ctx, out)
894
1101
  end
895
1102
 
896
- # No-op when +result+ is nil, matching
897
- # hegel_generate_string_result_free's documented no-op-on-NULL
898
- # contract (also safe on an already-freed, zeroed struct); not
899
- # translated, for the same reason as #context_free.
900
- def generate_string_result_free(ctx, result)
1103
+ # No-op when +result+ is nil, matching hegel_owned_string_free's
1104
+ # documented no-op-on-NULL contract (also safe on an already-freed,
1105
+ # zeroed struct); not translated, for the same reason as
1106
+ # #context_free. Before libhegel 0.46 this call was
1107
+ # hegel_generate_string_result_free.
1108
+ def owned_string_free(ctx, result)
901
1109
  # Removing this call only leaks native memory; Ruby has no way to
902
1110
  # observe the leak.
903
- @hegel_generate_string_result_free_fn.call(ctx, result) # mutineer:disable-line statement_removal
1111
+ @hegel_owned_string_free_fn.call(ctx, result) # mutineer:disable-line statement_removal
904
1112
  nil
905
1113
  end
906
1114
 
907
1115
  # Returns drawn bytes as a String, read via +len+ the same way
908
1116
  # #generate_string reads its own out-parameter: the header gives no
909
- # NUL-termination guarantee for this buffer either, so the length is
910
- # what makes the copy exact.
1117
+ # NUL-termination guarantee for this buffer, so the length is what
1118
+ # makes the copy exact.
911
1119
  #
912
1120
  # Unlike #generate_string, the result is never force-encoded.
913
1121
  # hegel_generate_bytes_result_t is documented as a byte buffer, not
@@ -923,7 +1131,7 @@ module Hegel
923
1131
  # header documents hegel_generate_bytes_result_free as safe on an
924
1132
  # already-freed (zeroed) struct too.
925
1133
  def generate_bytes(ctx, tc, min_size, max_size)
926
- out = RawResultStruct.new
1134
+ out = StringStruct.new
927
1135
  code = @hegel_generate_bytes_fn.call(ctx, tc, min_size, max_size, out)
928
1136
  LibHegel.check!(self, ctx, code)
929
1137
  out[:data].read_bytes(out[:len])
@@ -953,9 +1161,8 @@ module Hegel
953
1161
  # a NUL character in it reaches the engine instead of ending it.
954
1162
  def string_generator_regex(ctx, pattern, fullmatch, alphabet = nil)
955
1163
  out = FFI::MemoryPointer.new(:pointer)
956
- bytes = pattern.encode(Encoding::UTF_8)
957
1164
  code = @hegel_string_generator_regex_fn.call(
958
- ctx, bytes_to_pointer(bytes), bytes.bytesize, fullmatch, alphabet, out
1165
+ ctx, string_view(pattern.encode(Encoding::UTF_8)), fullmatch, alphabet, out
959
1166
  )
960
1167
  LibHegel.check!(self, ctx, code)
961
1168
  out.read_pointer
@@ -1088,8 +1295,93 @@ module Hegel
1088
1295
  [read_date(out[:date]), read_time(out[:time])]
1089
1296
  end
1090
1297
 
1298
+ # Returns a caller-owned grammar builder for +text+, a grammar in
1299
+ # Lark's syntax, released with #grammar_builder_free.
1300
+ def grammar_builder_new(ctx, text)
1301
+ out = FFI::MemoryPointer.new(:pointer)
1302
+ code = @hegel_grammar_builder_new_fn.call(ctx, string_view(text.encode(Encoding::UTF_8)), out)
1303
+ LibHegel.check!(self, ctx, code)
1304
+ out.read_pointer
1305
+ end
1306
+
1307
+ # The path of the +index+th file the grammar imports, or nil past the
1308
+ # end of the list. HEGEL_E_NOT_FOUND marks that end rather than an
1309
+ # error, so it is answered before LibHegel.check! sees it.
1310
+ def grammar_builder_file(ctx, builder, index)
1311
+ out = StringStruct.new
1312
+ code = @hegel_grammar_builder_file_fn.call(ctx, builder, index, out)
1313
+ return nil if code == HEGEL_E_NOT_FOUND
1314
+
1315
+ LibHegel.check!(self, ctx, code)
1316
+ read_string(out)
1317
+ end
1318
+
1319
+ # Supplies the +index+th file's +text+, or nil for a file that does
1320
+ # not exist, which the header keeps apart from an empty file.
1321
+ def grammar_builder_provide(ctx, builder, index, text)
1322
+ code = @hegel_grammar_builder_provide_fn.call(ctx, builder, index, optional_string(text))
1323
+ LibHegel.check!(self, ctx, code)
1324
+ nil
1325
+ end
1326
+
1327
+ def grammar_builder_free(ctx, builder)
1328
+ # Removing this call only leaks native memory; Ruby has no way to
1329
+ # observe the leak.
1330
+ @hegel_grammar_builder_free_fn.call(ctx, builder) # mutineer:disable-line statement_removal
1331
+ nil
1332
+ end
1333
+
1334
+ # Returns a caller-owned grammar compiled from +builder+, released
1335
+ # with #grammar_free. +start+ nil is the rule `start`; +alphabet+ is
1336
+ # an optional text generator handle. +names+ always goes over as a
1337
+ # non-NULL array, as hegel-rust passes it, even when it is empty.
1338
+ def grammar_new(ctx, builder, start, max_size, max_depth, alphabet, names)
1339
+ out = FFI::MemoryPointer.new(:pointer)
1340
+ names_array, names_slots = optional_name_array(names)
1341
+ code = @hegel_grammar_new_fn.call(
1342
+ ctx, builder, optional_string(start), max_size, max_depth, alphabet, names_array, names_slots.size, out
1343
+ )
1344
+ LibHegel.check!(self, ctx, code)
1345
+ out.read_pointer
1346
+ end
1347
+
1348
+ # The marker standing for the explicit terminal +name+ in the strings
1349
+ # drawn from +grammar+, copied out of the grammar's own buffer.
1350
+ def grammar_placeholder(ctx, grammar, name)
1351
+ out = StringStruct.new
1352
+ code = @hegel_grammar_placeholder_fn.call(ctx, grammar, string_view(name), out)
1353
+ LibHegel.check!(self, ctx, code)
1354
+ read_string(out)
1355
+ end
1356
+
1357
+ def grammar_free(ctx, grammar)
1358
+ # Removing this call only leaks native memory; Ruby has no way to
1359
+ # observe the leak.
1360
+ @hegel_grammar_free_fn.call(ctx, grammar) # mutineer:disable-line statement_removal
1361
+ nil
1362
+ end
1363
+
1364
+ # A drawn string of +grammar+ of at most +max_size+ characters outside
1365
+ # its markers, 0 meaning the grammar's own cap. Freed like
1366
+ # #generate_string's result.
1367
+ def generate_grammar(ctx, tc, grammar, max_size)
1368
+ out = StringStruct.new
1369
+ code = @hegel_generate_grammar_fn.call(ctx, tc, grammar, max_size, out)
1370
+ LibHegel.check!(self, ctx, code)
1371
+ read_string(out)
1372
+ ensure
1373
+ owned_string_free(ctx, out)
1374
+ end
1375
+
1091
1376
  private
1092
1377
 
1378
+ def retry_or_check!(ctx, code)
1379
+ return false if code == HEGEL_E_RETRY
1380
+
1381
+ LibHegel.check!(self, ctx, code)
1382
+ true
1383
+ end
1384
+
1093
1385
  # Resolves +symbol+ against @handle and wraps it as a callable
1094
1386
  # FFI::Function, the direct form #initialize's own comment explains
1095
1387
  # the choice of.
@@ -1103,8 +1395,8 @@ module Hegel
1103
1395
  end
1104
1396
 
1105
1397
  # Copies +bytes+ into a freshly allocated buffer, for
1106
- # #generate_integer_big's min_value/max_value: a const uint8_t*
1107
- # argument libhegel reads from, the opposite direction of an
1398
+ # #generate_integer_big's min_value/max_value and for #write_string:
1399
+ # input bytes libhegel reads from, the opposite direction of an
1108
1400
  # out-parameter buffer (which libhegel writes into, and the caller
1109
1401
  # then reads).
1110
1402
  def bytes_to_pointer(bytes)
@@ -1113,21 +1405,50 @@ module Hegel
1113
1405
  ptr
1114
1406
  end
1115
1407
 
1116
- # Packs +names+ (an Array of Ruby Strings) into a const char *const *
1117
- # for #new_state_machine's rule_names/invariant_names arguments: one
1118
- # address per name, written into a single buffer via
1119
- # write_array_of_pointer.
1408
+ # Writes +string+'s bytes into +struct+, a StringStruct, as a borrowed
1409
+ # hegel_string_t input. The bytes go over with their length and no
1410
+ # terminator, so an interior NUL reaches the engine. An empty string
1411
+ # gets a zero-size buffer: the header accepts any pointer for a
1412
+ # length of zero.
1120
1413
  #
1121
- # Returns [pointer, kept_alive]. FFI::MemoryPointer.from_string
1122
- # copies each name into a fresh native buffer that Ruby object owns
1123
- # -- unlike a pointer built straight off a Ruby String's own bytes,
1124
- # nothing here depends on the original +names+ Strings staying live
1125
- # -- so what must stay live until the native call reads them is the
1126
- # per-name FFI::MemoryPointer array itself; write_array_of_pointer
1127
- # only copies the addresses those objects report, not a reference to
1128
- # the objects, so the caller keeping +kept_alive+ as a live local for
1129
- # the length of the call is what keeps the buffers each one owns
1130
- # from being garbage-collected first.
1414
+ # FFI::Struct keeps a reference to a pointer it stores in a field, so
1415
+ # the copied buffer lives as long as +struct+ does. Measured on ffi
1416
+ # 1.17.4: after a full GC, a WeakRef to a buffer held only by a struct
1417
+ # field is still alive, and one to a buffer with no holder is not. A
1418
+ # caller keeps the struct live for the native call by passing it as an
1419
+ # argument.
1420
+ def write_string(struct, string)
1421
+ struct[:data] = bytes_to_pointer(string)
1422
+ struct[:len] = string.bytesize
1423
+ struct
1424
+ end
1425
+
1426
+ # A hegel_string_t for an ordinary input argument, passed by value.
1427
+ def string_view(string)
1428
+ write_string(StringStruct.new, string)
1429
+ end
1430
+
1431
+ # A pointer to a hegel_string_t for an optional input argument: nil
1432
+ # for an absent value, which marshals to NULL, and a struct
1433
+ # otherwise. The header separates the two cases: NULL means absent,
1434
+ # and a pointer to an empty string means present and empty. "" for
1435
+ # #settings_set_database depends on that difference.
1436
+ def optional_string(string)
1437
+ string && string_view(string)
1438
+ end
1439
+
1440
+ # Packs +names+ (an Array of Ruby Strings) into a contiguous array of
1441
+ # hegel_string_t for #new_state_machine's rule_names/invariant_names
1442
+ # arguments: one StringStruct per name, each written in place over
1443
+ # its own slot of a single buffer.
1444
+ #
1445
+ # Returns [pointer, kept_alive]. #write_string copies each name into
1446
+ # a fresh native buffer -- nothing here depends on the original
1447
+ # +names+ Strings staying live -- and each slot's struct holds a
1448
+ # reference to that buffer. The array buffer holds only addresses,
1449
+ # not references, so the caller keeping +kept_alive+ (the slot
1450
+ # structs) as a live local for the length of the call is what keeps
1451
+ # the buffers each one owns from being garbage-collected first.
1131
1452
  #
1132
1453
  # Empty +names+ maps to [nil, []] -- NULL and (by the caller passing
1133
1454
  # names.size, 0) the header's own contract for invariant_names' "no
@@ -1142,10 +1463,21 @@ module Hegel
1142
1463
  # hegel_new_state_machine reads zero names either way.
1143
1464
  return [nil, []] if names.empty? # mutineer:disable-line statement_removal
1144
1465
 
1145
- pointers = names.map { |name| FFI::MemoryPointer.from_string(name) }
1146
- array = FFI::MemoryPointer.new(:pointer, names.size)
1147
- array.write_array_of_pointer(pointers)
1148
- [array, pointers]
1466
+ array = FFI::MemoryPointer.new(StringStruct, names.size)
1467
+ slots = names.each_with_index.map do |name, i|
1468
+ write_string(StringStruct.new(array + (i * StringStruct.size)), name)
1469
+ end
1470
+ [array, slots]
1471
+ end
1472
+
1473
+ # #pack_name_array for an argument where nil and an empty Array mean
1474
+ # different things: nil stays NULL, and an empty Array becomes a
1475
+ # non-NULL pointer, which #pack_name_array alone would turn into NULL.
1476
+ def optional_name_array(names)
1477
+ return [nil, []] unless names
1478
+
1479
+ array, slots = pack_name_array(names)
1480
+ [array || FFI::MemoryPointer.new(StringStruct), slots]
1149
1481
  end
1150
1482
 
1151
1483
  # Packs +flags+ (an Array of true/false) into a const bool * buffer,
@@ -1222,26 +1554,29 @@ module Hegel
1222
1554
  [struct[:hour], struct[:minute], struct[:second], struct[:nanosecond]]
1223
1555
  end
1224
1556
 
1225
- # Reads +out+'s const char* out-parameter into a Ruby String, or
1226
- # returns nil if libhegel left it NULL. Shared by #run_result_error,
1227
- # #failure_reproduction_blob, and #failure_caveat, the out-parameters
1228
- # the header documents as nullable, so both branches only need to be
1229
- # exercised once between the call sites rather than at each one.
1557
+ # Reads +out+'s const hegel_string_t* out-parameter into a Ruby
1558
+ # String, or returns nil if libhegel left it NULL. Shared by
1559
+ # #run_result_error, #failure_reproduction_blob, and #failure_caveat,
1560
+ # the out-parameters the header documents as nullable, so both
1561
+ # branches only need to be exercised once between the call sites
1562
+ # rather than at each one.
1230
1563
  def nullable_out_string(out)
1231
1564
  ptr = out.read_pointer
1232
- ptr.null? ? nil : utf8(ptr)
1233
- end
1234
-
1235
- # Reads the NUL-terminated string at +pointer+ and labels it UTF-8.
1236
- # The header calls every string libhegel hands back valid UTF-8, but
1237
- # FFI::Pointer#read_string labels what it reads ASCII-8BIT. A caller
1238
- # who matches an error message against a pattern of their own then
1239
- # gets Encoding::CompatibilityError rather than an answer, and the
1240
- # engine's own messages do carry characters outside ASCII: the
1241
- # FilterTooMuch health check writes an em dash into the sentence a
1242
- # caller is most likely to read.
1243
- def utf8(pointer)
1244
- pointer.read_string.force_encoding(Encoding::UTF_8)
1565
+ ptr.null? ? nil : read_string(StringStruct.new(ptr))
1566
+ end
1567
+
1568
+ # Copies the +len+ bytes at +struct+'s data pointer and labels them
1569
+ # UTF-8. The header calls every string libhegel hands back valid
1570
+ # UTF-8, but FFI::Pointer#read_bytes labels what it reads ASCII-8BIT.
1571
+ # A caller who matches an error message against a pattern of their
1572
+ # own then gets Encoding::CompatibilityError rather than an answer,
1573
+ # and the engine's own messages do carry characters outside ASCII:
1574
+ # the FilterTooMuch health check writes an em dash into the sentence
1575
+ # a caller is most likely to read. The length decides the copy, not
1576
+ # the trailing NUL the header also promises, since a string can hold
1577
+ # an interior NUL.
1578
+ def read_string(struct)
1579
+ struct[:data].read_bytes(struct[:len]).force_encoding(Encoding::UTF_8)
1245
1580
  end
1246
1581
  end
1247
1582
  end