hegeltest 0.3.0 → 0.3.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b20fa55081891dc4db4f8b9dd6611a39793214c74b76946da062058a9987315f
4
- data.tar.gz: d495a52e3da347f8632aca40c45c68797f32fe683d67a2ab961df4d49ae33338
3
+ metadata.gz: ecb67897e8ff8580eee45713f3ef1bf6f4f17878f5206830f40a47761ed1d193
4
+ data.tar.gz: 68143dade773e79ac88b16197a3213717808645b5e47f6799560867cac1c1009
5
5
  SHA512:
6
- metadata.gz: 336a4583feb256103314cfbd9029d5752e1e223738e9f12337541aa487cfcddd9d3d60432ca4629aca3ef068ba6238d742c244a004df751a886bcc355cc019f2
7
- data.tar.gz: 1ef3026df63b5d814ddd9379af418b350b14874330af9ba449e5bd1843c1a9dee82906075da6d677351f8baa11cc300a228544b16b210534735d4fede93ddd7f
6
+ metadata.gz: e91326c8d22876cddef53a04b3f62a6ff5b91929c70588e5117f1dbdb3089de014b4721c6058d0016a6b687585db6bfb72d90d6826aa04dc4e849c6a37090216
7
+ data.tar.gz: cd65227abec3bc433db3b1acc900050cfd403f8c76046309cff7c703c3025d99b665194b2c20b4b9627ca3b4d64ef5221e58779dfe373d1364f27b88dc579593
data/CHANGELOG.md CHANGED
@@ -2,6 +2,105 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.4] - 2026-10-20
6
+
7
+ ### Engine: libhegel 0.47.2 (was 0.47.1)
8
+
9
+ The [libhegel 0.47.2 release notes](https://github.com/hegeldev/hegel/releases/tag/libhegel/0.47.2)
10
+ describe 0.47.2. It is the first engine release from the hegeldev/hegel
11
+ repository.
12
+
13
+ Each platform gem carries `libhegel` 0.47.2. The engine does not change its
14
+ C interface. A caller has nothing to change in code, and gets the same
15
+ results. The engine changes one text: the message of an internal engine
16
+ error now gives <https://github.com/hegeldev/hegel/issues> as the place to
17
+ report the problem, not the hegel-rust issue tracker.
18
+
19
+ ### Development
20
+
21
+ - `rake libhegel:fetch` and `rake libhegel:fetch_all` download libhegel
22
+ 0.47.2 and later from the hegeldev/hegel releases, and earlier versions
23
+ from the hegeldev/hegel-rust releases.
24
+
25
+ ## [0.3.3] - 2026-10-17
26
+
27
+ ### Engine: libhegel 0.47.1 (was 0.45.13)
28
+
29
+ The [libhegel 0.47.1 release notes](https://github.com/hegeldev/hegel-rust/releases/tag/libhegel-v0.47.1)
30
+ describe 0.47.1 alone. The
31
+ [engine changelog](https://github.com/hegeldev/hegel-rust/blob/libhegel-v0.47.1/hegel-c/CHANGELOG.md)
32
+ describes each engine release after 0.45.13, up to 0.47.1.
33
+
34
+ Each platform gem carries `libhegel` 0.47.1. The engine changes its C
35
+ interface: every string is now a `hegel_string_t`, a pointer and a length.
36
+ These bindings follow that change. A caller has nothing to change in code.
37
+ The engine move changes these results for a caller:
38
+
39
+ - A run with the default `report_multiple_failures: false` that finds more
40
+ than one distinct failure shrinks only the smallest of them and reports
41
+ it. Before, the engine shrank each failure and then reported one. The
42
+ example database keeps every distinct failure, as found. Such a run calls
43
+ the test body fewer times: for a body with two distinct failures and
44
+ `test_cases: 100`, 73 to 131 calls over 20 seeded runs, where 0.45.13
45
+ made 157 to 202. Both versions reported the same failure in each run.
46
+ - A string that holds a NUL character, such as a `target` label or a
47
+ `database:` path, reaches the engine whole. Before, it raised
48
+ `ArgumentError` ("string contains null byte") from the `ffi` gem.
49
+
50
+ ## [0.3.2] - 2026-10-16
51
+
52
+ ### Engine: libhegel 0.45.13 (was 0.45.7)
53
+
54
+ The [libhegel 0.45.13 release notes](https://github.com/hegeldev/hegel-rust/releases/tag/libhegel-v0.45.13)
55
+ describe 0.45.13 alone. The
56
+ [engine changelog](https://github.com/hegeldev/hegel-rust/blob/libhegel-v0.45.13/hegel-c/CHANGELOG.md)
57
+ describes each engine release after 0.45.7, up to 0.45.13.
58
+
59
+ Each platform gem carries `libhegel` 0.45.13. The engine only adds to its C
60
+ interface, and these bindings do not use the addition. A caller has nothing
61
+ to change in code. The engine changes these results for a caller:
62
+
63
+ - A run with a `seed:` can draw different values than it did before. The
64
+ engine changed how it copies values between spans that have the same
65
+ label, and how it draws an integer from a range wider than 128 bits.
66
+ - A failure that depends on an integer that is a multiple of a number, such
67
+ as 6, shrinks to the smallest such value more often. Before, the shrinker
68
+ could stop at a larger multiple, such as 78.
69
+ - A run of 1,000 test cases or more starts to use `target` scores soon
70
+ after it starts. Before, it started after half of the test cases.
71
+ - The `note:` line with the engine's nondeterminism caveat shows only for a
72
+ failure that the engine saw pass at least one time. Before, every failure
73
+ of a run that the engine treated as nondeterministic showed it.
74
+ - `reproduce_failure:` stops a replay when the test body draws differently
75
+ from the recorded choices. If no replay reproduces the failure, the run
76
+ raises `Hegel::Error` with the message that says that the blob did not
77
+ reproduce a failure, and the engine writes one line to standard error
78
+ that names the position where the draws left the recorded choices.
79
+ Before, the engine continued the replay with new draws and could report a
80
+ different failure as the failure of the blob.
81
+
82
+ ## [0.3.1] - 2026-10-15
83
+
84
+ ### Engine: libhegel 0.45.7 (was 0.45.4)
85
+
86
+ The [libhegel 0.45.7 release notes](https://github.com/hegeldev/hegel-rust/releases/tag/libhegel-v0.45.7)
87
+ describe 0.45.7 alone. The
88
+ [engine changelog](https://github.com/hegeldev/hegel-rust/blob/libhegel-v0.45.7/hegel-c/CHANGELOG.md)
89
+ describes each engine release after 0.45.4, up to 0.45.7.
90
+
91
+ Each platform gem carries `libhegel` 0.45.7. The engine only adds to its C
92
+ interface, and these bindings do not use the addition. A caller has nothing
93
+ to change in code. The engine changes these messages for a caller:
94
+
95
+ - `from_regex` with a pattern that no string matches, such as `"(?!)"`,
96
+ raises `Hegel::Error` with a message that names the pattern and says
97
+ that it `matches no string`. Before, the run raised `Hegel::Error` with
98
+ a message that starts with `Unsatisfiable`.
99
+ - The FilterTooMuch health check message says how to suppress the check
100
+ with the `suppress_health_check` setting, and it says "rejected by
101
+ assume() or a filter". Before, it showed Rust syntax. The
102
+ `FailedHealthCheck:` and `Unsatisfiable` prefixes do not change.
103
+
5
104
  ## [0.3.0] - 2026-10-14
6
105
 
7
106
  ### Engine: libhegel 0.45.4 (was 0.45.1)
@@ -174,6 +273,10 @@ The engine is called through the `ffi` gem. Each platform gem carries the
174
273
  matching `libhegel` 0.32.5 build. The platform-independent gem carries none,
175
274
  so a run on it needs `HEGEL_LIBHEGEL_PATH` pointing at a local build.
176
275
 
276
+ [0.3.4]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.4
277
+ [0.3.3]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.3
278
+ [0.3.2]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.2
279
+ [0.3.1]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.1
177
280
  [0.3.0]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.0
178
281
  [0.2.1]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.2.1
179
282
  [0.2.0]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.2.0
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  > there reviews or endorses it.
8
8
  >
9
9
  > The official implementations are
10
- > [Rust](https://github.com/hegeldev/hegel-rust),
10
+ > [Rust](https://github.com/hegeldev/hegel/tree/main/frontends/rust),
11
11
  > [Go](https://github.com/hegeldev/hegel-go),
12
12
  > [C++](https://github.com/hegeldev/hegel-cpp),
13
13
  > [TypeScript](https://github.com/hegeldev/hegel-typescript),
data/docs/README.md CHANGED
@@ -31,6 +31,8 @@ An index of the design records for `hegel-ruby`.
31
31
  - [0022: Keep microsecond times over a nanosecond engine](adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md)
32
32
  - [0023: Leave unset settings to the engine's profile](adr/0023-leave-unset-settings-to-the-engines-profile.md)
33
33
  - [0024: Release when a merge changes the gem version](adr/0024-release-when-a-merge-changes-the-gem-version.md)
34
+ - [0025: Copy each string input into a hegel_string_t](adr/0025-copy-each-string-input-into-a-hegel-string-t.md)
35
+ - [0026: Select the engine release repository by version](adr/0026-select-the-engine-release-repository-by-version.md)
34
36
 
35
37
  A new decision gets a new record. A changed decision supersedes the old
36
38
  record instead of editing it, so the history stays readable.
@@ -0,0 +1,61 @@
1
+ # 0025: Copy each string input into a hegel_string_t
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ libhegel 0.46.0 changed every string in its C interface to a
10
+ `hegel_string_t`, a struct of a data pointer and a byte length. An ordinary
11
+ input goes by value. An optional input goes through a pointer to the
12
+ struct, and a NULL pointer means that the value is absent. An output is a
13
+ struct that the engine writes. Before 0.46, a string input was a
14
+ NUL-terminated `const char *`, which the `ffi` gem marshals from a Ruby
15
+ String with the `:string` type. That type stops at the first NUL, and
16
+ `ffi` 1.17.4 raises `ArgumentError` for a String with an interior NUL.
17
+
18
+ The struct is 16 bytes on a 64-bit platform. The calling conventions do not
19
+ pass a struct of that size in the same way: x86-64 System V and AArch64
20
+ pass it in two registers, and Windows x64 passes it through a hidden
21
+ pointer. This is inferred from the ABI documents of each platform, not
22
+ measured here; the CI runs the suite on each of them. A binding that
23
+ declared two scalar arguments in place of the struct would match some of
24
+ these conventions and not others.
25
+
26
+ hegel-rust's own frontend (`src/ffi.rs` at `libhegel-v0.47.1`) borrows the
27
+ bytes of a Rust string for the length of the call.
28
+
29
+ ## Decision
30
+
31
+ `LibHegel::Real` declares each `hegel_string_t` argument as
32
+ `StringStruct.by_value`, and lets libffi apply the calling convention of the
33
+ platform. It declares each optional string argument as `:pointer`, and
34
+ passes `nil` for an absent value and a `StringStruct` for a present one, so
35
+ that `""` stays different from `nil`. `settings_set_database` needs that
36
+ difference: `""` turns the database off, and `nil` keeps the engine's
37
+ default directory.
38
+
39
+ Each input string is copied into a native buffer that the struct holds. The
40
+ binding does not point the struct at the memory of the Ruby String: a copy
41
+ does not depend on how long the garbage collector keeps that memory in
42
+ place (inferred: compaction can move the contents of a short String).
43
+ Measured on `ffi` 1.17.4, a struct field keeps the buffer it holds alive
44
+ through a full garbage collection. The struct is an argument of the native
45
+ call, so the buffer lives until the call returns.
46
+
47
+ An output is read by its length, not up to its trailing NUL, because an
48
+ engine string can hold an interior NUL.
49
+
50
+ ## Consequences
51
+
52
+ A string that holds a NUL character, for example a `target` label, reaches
53
+ the engine whole. Before, it raised `ArgumentError`.
54
+
55
+ Each string input costs one native allocation and one copy. The strings that
56
+ cross on each test case are short: an origin for an interesting case, and
57
+ a label for each `target` call.
58
+
59
+ The rename of `hegel_generate_string_result_free` to
60
+ `hegel_owned_string_free` renames the binding method to `owned_string_free`.
61
+ `LibHegel::METHODS`, the Fake, and `sig/hegel.rbs` follow it.
@@ -0,0 +1,47 @@
1
+ # 0026: Select the engine release repository by version
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ Up to libhegel 0.47.1, hegeldev/hegel-rust published each engine release
10
+ with the tag `libhegel-v<version>`. From 0.47.2, the engine and the Rust
11
+ frontend live in the monorepo hegeldev/hegel, which publishes each engine
12
+ release with the tag `libhegel/<version>`. hegeldev/hegel-rust is now an
13
+ archive and gets no new release. The asset names did not change: `hegel.h`,
14
+ `libhegel-<os>-<arch>.<ext>`, and `<asset>.sha256`.
15
+
16
+ Measured on 2026-10-19 with HTTP requests to the release download URLs:
17
+ hegeldev/hegel does not carry the assets of the old releases
18
+ (`libhegel/0.47.1/hegel.h` gives HTTP 404). Measured on 2026-10-20: each of
19
+ the five platform assets of 0.47.2, and its `.sha256` file, gives HTTP 200
20
+ from hegeldev/hegel.
21
+
22
+ ## Decision
23
+
24
+ `Hegel::LibhegelFetch.release_base` selects the repository from the version.
25
+ A version of 0.47.2 or later downloads from
26
+ `https://github.com/hegeldev/hegel/releases/download/libhegel/<version>`. An
27
+ earlier version downloads from
28
+ `https://github.com/hegeldev/hegel-rust/releases/download/libhegel-v<version>`.
29
+
30
+ The rule keeps the old releases reachable. A contributor who checks out an
31
+ old commit of this gem can still run `rake libhegel:fetch` for the version
32
+ that commit pins.
33
+
34
+ One alternative was refused: a single base URL for the new repository. It
35
+ is shorter, but it breaks `rake libhegel:fetch` on every old commit, because
36
+ the new repository does not carry the old assets.
37
+
38
+ ## Consequences
39
+
40
+ The CHANGELOG rule in CLAUDE.md links the engine release notes and
41
+ `libhegel/CHANGELOG.md` in hegeldev/hegel for an engine move to 0.47.2 or
42
+ later. The reference paths in CLAUDE.md and `docs/architecture.md` point into
43
+ the monorepo tree: `libhegel/` for the engine, and `frontends/rust/` for the
44
+ Rust frontend.
45
+
46
+ If the old releases move to hegeldev/hegel later, the legacy branch can go,
47
+ and this record gets a successor.
data/docs/architecture.md CHANGED
@@ -43,10 +43,13 @@ schedules it.
43
43
 
44
44
  ## Where meaning comes from
45
45
 
46
- libhegel is hegel-rust's native engine. hegel-rust's Cargo workspace builds
47
- it from the `hegel-c` directory, as the crate `hegeltest-c`, whose header is
48
- `hegel-c/include/hegel.h`. That header defines the ABI this gem calls: what
49
- each function does, what its error codes mean, and who owns each pointer.
46
+ libhegel is the native engine of every Hegel frontend. Since 0.47.2, the
47
+ hegeldev/hegel Cargo workspace builds it from the `libhegel` directory, as
48
+ the crate `hegeltest-c`, whose header is `libhegel/include/hegel.h`. The
49
+ same workspace holds the Rust frontend, hegel-rust, at `frontends/rust`.
50
+ Releases up to 0.47.1 came from hegeldev/hegel-rust, where the engine was at
51
+ `hegel-c`. The header defines the ABI this gem calls: what each function
52
+ does, what its error codes mean, and who owns each pointer.
50
53
 
51
54
  The Java, Go, TypeScript, OCaml, and C++ implementations bind the same
52
55
  engine. Each works under a constraint hegel-rust does not have: a dynamic
@@ -171,12 +174,16 @@ fields.
171
174
 
172
175
  ## Answered
173
176
 
174
- A drawn string arrives as a pointer and a length, is not NUL-terminated,
175
- and can hold interior NUL bytes, because the drawn alphabet can include
176
- U+0000. Read by length. Measured against 0.45.0: an alphabet pinned to
177
- U+0000 produces a three-byte draw that reads as `""` when taken up to the
178
- first NUL and as three NUL bytes when taken by its reported length. Every
179
- string observed reports `valid_encoding?` after `force_encoding(UTF-8)`.
177
+ A drawn string arrives as a pointer and a length, and can hold interior
178
+ NUL bytes, because the drawn alphabet can include U+0000. Read by length.
179
+ Before libhegel 0.46 the string had no NUL terminator. Since 0.46 every
180
+ string the engine hands back is a `hegel_string_t` with a trailing NUL that
181
+ its length excludes, but an interior NUL still ends the string early for a
182
+ reader that stops at the first NUL. Measured against 0.45.0: an alphabet
183
+ pinned to U+0000 produces a three-byte draw that reads as `""` when taken
184
+ up to the first NUL and as three NUL bytes when taken by its reported
185
+ length. Every string observed reports `valid_encoding?` after
186
+ `force_encoding(UTF-8)`.
180
187
 
181
188
  Prism recovers a draw's assignment target through a method chain, an
182
189
  instance-variable assignment, and an assignment spanning several lines.
@@ -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,7 +77,7 @@ 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)
@@ -93,13 +95,13 @@ module Hegel
93
95
  "hegel_settings_set_derandomize", [:pointer, :pointer, :bool], :int32
94
96
  )
95
97
  @hegel_settings_set_database_fn = bind(
96
- "hegel_settings_set_database", [:pointer, :pointer, :string], :int32
98
+ "hegel_settings_set_database", [:pointer, :pointer, :pointer], :int32
97
99
  )
98
100
  @hegel_settings_set_report_multiple_failures_fn = bind(
99
101
  "hegel_settings_set_report_multiple_failures", [:pointer, :pointer, :bool], :int32
100
102
  )
101
103
  @hegel_settings_set_database_key_fn = bind(
102
- "hegel_settings_set_database_key", [:pointer, :pointer, :string], :int32
104
+ "hegel_settings_set_database_key", [:pointer, :pointer, :pointer], :int32
103
105
  )
104
106
  @hegel_settings_set_phases_fn = bind(
105
107
  "hegel_settings_set_phases", [:pointer, :pointer, :uint32], :int32
@@ -112,7 +114,7 @@ module Hegel
112
114
  "hegel_run_start", [:pointer, :pointer, :pointer, :pointer, :pointer], :int32
113
115
  )
114
116
  @hegel_run_start_blob_fn = bind(
115
- "hegel_run_start_blob", [:pointer, :pointer, :string, :pointer, :pointer, :pointer], :int32
117
+ "hegel_run_start_blob", [:pointer, :pointer, StringStruct.by_value, :pointer, :pointer, :pointer], :int32
116
118
  )
117
119
  @hegel_next_test_case_fn = bind("hegel_next_test_case", [:pointer, :pointer, :pointer], :int32)
118
120
  @hegel_test_case_should_capture_fn = bind(
@@ -120,8 +122,8 @@ module Hegel
120
122
  )
121
123
  @hegel_run_free_fn = bind("hegel_run_free", [:pointer, :pointer], :int32)
122
124
  @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)
125
+ @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :pointer], :int32)
126
+ @hegel_target_fn = bind("hegel_target", [:pointer, :pointer, :double, StringStruct.by_value], :int32)
125
127
 
126
128
  @hegel_run_result_fn = bind("hegel_run_result", [:pointer, :pointer, :pointer], :int32)
127
129
  @hegel_run_result_free_fn = bind("hegel_run_result_free", [:pointer, :pointer], :int32)
@@ -159,7 +161,7 @@ module Hegel
159
161
  )
160
162
  @hegel_collection_more_fn = bind("hegel_collection_more", [:pointer, :pointer, :pointer, :pointer], :int32)
161
163
  @hegel_collection_reject_fn = bind(
162
- "hegel_collection_reject", [:pointer, :pointer, :pointer, :string], :int32
164
+ "hegel_collection_reject", [:pointer, :pointer, :pointer, :pointer], :int32
163
165
  )
164
166
  @hegel_collection_free_fn = bind("hegel_collection_free", [:pointer, :pointer], :int32)
165
167
 
@@ -196,14 +198,14 @@ module Hegel
196
198
  [:pointer, :pointer, :uint32, :double, :double, :bool, :bool, :bool, :bool, :double, :pointer], :int32
197
199
  )
198
200
 
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).
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).
203
205
  @hegel_string_generator_text_fn = bind(
204
206
  "hegel_string_generator_text",
205
- [:pointer, :uint64, :uint64, :string, :uint32, :uint32,
206
- :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t,
207
+ [:pointer, :uint64, :uint64, :pointer, :uint32, :uint32,
208
+ :pointer, :size_t, :pointer, :size_t, StringStruct.by_value, StringStruct.by_value,
207
209
  :pointer],
208
210
  :int32
209
211
  )
@@ -211,9 +213,7 @@ module Hegel
211
213
  @hegel_generate_string_fn = bind(
212
214
  "hegel_generate_string", [:pointer, :pointer, :pointer, :pointer], :int32
213
215
  )
214
- @hegel_generate_string_result_free_fn = bind(
215
- "hegel_generate_string_result_free", [:pointer, :pointer], :int32
216
- )
216
+ @hegel_owned_string_free_fn = bind("hegel_owned_string_free", [:pointer, :pointer], :int32)
217
217
 
218
218
  @hegel_generate_bytes_fn = bind(
219
219
  "hegel_generate_bytes", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
@@ -223,7 +223,7 @@ module Hegel
223
223
  )
224
224
 
225
225
  @hegel_string_generator_regex_fn = bind(
226
- "hegel_string_generator_regex", [:pointer, :pointer, :size_t, :bool, :pointer, :pointer], :int32
226
+ "hegel_string_generator_regex", [:pointer, StringStruct.by_value, :bool, :pointer, :pointer], :int32
227
227
  )
228
228
  @hegel_string_generator_email_fn = bind("hegel_string_generator_email", [:pointer, :pointer], :int32)
229
229
  @hegel_string_generator_url_fn = bind("hegel_string_generator_url", [:pointer, :pointer], :int32)
@@ -273,18 +273,18 @@ module Hegel
273
273
  # before returning, since the header documents that buffer as
274
274
  # borrowed and invalidated by the next call taking the same context.
275
275
  def context_last_error(ctx)
276
- utf8(@hegel_context_last_error_fn.call(ctx))
276
+ read_string(@hegel_context_last_error_fn.call(ctx))
277
277
  end
278
278
 
279
279
  # Returns the loaded engine's version string, or raises the
280
280
  # exception LibHegel.check! translates this call's result code to.
281
281
  def version(ctx)
282
- out = FFI::MemoryPointer.new(:pointer)
282
+ out = StringStruct.new
283
283
  code = @hegel_version_fn.call(ctx, out)
284
284
  # This call takes only ctx and this method's own out argument; neither
285
285
  # makes the real engine return anything but HEGEL_OK.
286
286
  LibHegel.check!(self, ctx, code) # mutineer:disable-line statement_removal
287
- utf8(out.read_pointer)
287
+ read_string(out)
288
288
  end
289
289
 
290
290
  # Returns a settings handle initialized from the engine's resolved
@@ -334,11 +334,11 @@ module Hegel
334
334
  end
335
335
 
336
336
  # +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.
337
+ # including "" to disable the database. The header takes an optional
338
+ # string here, so nil goes over as a NULL pointer and "" as a pointer
339
+ # to an empty string; see #optional_string.
340
340
  def settings_set_database(ctx, s, database)
341
- code = @hegel_settings_set_database_fn.call(ctx, s, database)
341
+ code = @hegel_settings_set_database_fn.call(ctx, s, optional_string(database))
342
342
  LibHegel.check!(self, ctx, code)
343
343
  nil
344
344
  end
@@ -350,10 +350,10 @@ module Hegel
350
350
  end
351
351
 
352
352
  # +key+ may be nil, which the header documents as clearing the key
353
- # (the default); nil marshals to NULL for this :string argument, the
353
+ # (the default); nil goes over as NULL through #optional_string, the
354
354
  # same as #settings_set_database's own nilable database argument.
355
355
  def settings_set_database_key(ctx, s, key)
356
- code = @hegel_settings_set_database_key_fn.call(ctx, s, key)
356
+ code = @hegel_settings_set_database_key_fn.call(ctx, s, optional_string(key))
357
357
  LibHegel.check!(self, ctx, code)
358
358
  nil
359
359
  end
@@ -393,7 +393,7 @@ module Hegel
393
393
  # version.
394
394
  def run_start_blob(ctx, settings, blob)
395
395
  out = FFI::MemoryPointer.new(:pointer)
396
- code = @hegel_run_start_blob_fn.call(ctx, settings, blob, nil, nil, out)
396
+ code = @hegel_run_start_blob_fn.call(ctx, settings, string_view(blob), nil, nil, out)
397
397
  LibHegel.check!(self, ctx, code)
398
398
  out.read_pointer
399
399
  end
@@ -442,9 +442,9 @@ module Hegel
442
442
  # +origin+ must be non-nil only when +status+ is
443
443
  # HEGEL_STATUS_INTERESTING, per the header; this layer neither builds
444
444
  # nor validates that string, only passes through what the caller
445
- # supplies. nil marshals to NULL for this :string argument.
445
+ # supplies. nil goes over as NULL through #optional_string.
446
446
  def mark_complete(ctx, tc, status, origin)
447
- code = @hegel_mark_complete_fn.call(ctx, tc, status, origin)
447
+ code = @hegel_mark_complete_fn.call(ctx, tc, status, optional_string(origin))
448
448
  LibHegel.check!(self, ctx, code)
449
449
  nil
450
450
  end
@@ -456,7 +456,7 @@ module Hegel
456
456
  # checked here, matching how this layer leaves every other
457
457
  # argument-shape rule to the engine's own HEGEL_E_INVALID_ARG.
458
458
  def target(ctx, tc, value, label)
459
- code = @hegel_target_fn.call(ctx, tc, value, label)
459
+ code = @hegel_target_fn.call(ctx, tc, value, string_view(label))
460
460
  LibHegel.check!(self, ctx, code)
461
461
  nil
462
462
  end
@@ -538,10 +538,10 @@ module Hegel
538
538
  # returning, since it is owned by the failure and only valid until
539
539
  # #failure_free.
540
540
  def failure_origin(ctx, f)
541
- out = FFI::MemoryPointer.new(:pointer)
541
+ out = StringStruct.new
542
542
  code = @hegel_failure_origin_fn.call(ctx, f, out)
543
543
  LibHegel.check!(self, ctx, code)
544
- utf8(out.read_pointer)
544
+ read_string(out)
545
545
  end
546
546
 
547
547
  # Returns nil when libhegel produced no reproduction blob for this
@@ -659,12 +659,12 @@ module Hegel
659
659
  end
660
660
 
661
661
  # 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
662
+ # +why+ is an optional human-readable reason (nil goes over as NULL
663
+ # through #optional_string, which the header allows); the header
664
664
  # documents it as validated but reserved for future rejection
665
665
  # diagnostics, unused today.
666
666
  def collection_reject(ctx, tc, collection, why = nil)
667
- code = @hegel_collection_reject_fn.call(ctx, tc, collection, why)
667
+ code = @hegel_collection_reject_fn.call(ctx, tc, collection, optional_string(why))
668
668
  LibHegel.check!(self, ctx, code)
669
669
  nil
670
670
  end
@@ -733,9 +733,9 @@ module Hegel
733
733
 
734
734
  # Returns a caller-owned state-machine handle, released separately
735
735
  # 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
736
+ # each an Array of Ruby Strings, packed into the hegel_string_t arrays
737
+ # hegel_new_state_machine expects by #pack_name_array; see that
738
+ # method's own comment for how and why. +always_check+ holds one
739
739
  # flag per invariant name. Validating +rule_names+ as non-empty (the
740
740
  # header's own requirement) is left to the caller, the same division
741
741
  # of labor #new_collection leaves to the caller for its own
@@ -840,17 +840,18 @@ module Hegel
840
840
 
841
841
  # Returns a caller-owned string generator handle, released
842
842
  # 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.
843
+ # 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.
849
850
  def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF)
850
851
  out = FFI::MemoryPointer.new(:pointer)
851
852
  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,
853
+ ctx, min_size, max_size, optional_string(codec), min_codepoint, max_codepoint,
854
+ nil, 0, nil, 0, StringStruct.new, StringStruct.new,
854
855
  out
855
856
  )
856
857
  LibHegel.check!(self, ctx, code)
@@ -867,47 +868,46 @@ module Hegel
867
868
  nil
868
869
  end
869
870
 
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.
871
+ # Returns a drawn String, labelled UTF-8 by #read_string. The draw can
872
+ # hold interior NUL bytes, since the drawn alphabet can include
873
+ # U+0000, so #read_string copies exactly the returned +len+ bytes.
875
874
  #
876
875
  # The native buffer is released in +ensure+ via
877
- # #generate_string_result_free, called directly from here rather
876
+ # #owned_string_free, called directly from here rather
878
877
  # than left to the caller: nothing outside this file may hold or
879
878
  # read the raw struct (ffi is confined to this file), so unlike
880
879
  # #new_collection or #string_generator_text, the freeable handle
881
880
  # here never leaves this method. Freeing is safe even when the draw
882
881
  # above raised: +out+ starts zero-filled (FFI::Struct.new allocates
883
882
  # cleared memory) and libhegel only writes into it on success, so
884
- # the struct #generate_string_result_free sees here is always
883
+ # the struct #owned_string_free sees here is always
885
884
  # either a completed draw or the all-zero state the header
886
885
  # documents as already safe to free.
887
886
  def generate_string(ctx, tc, generator)
888
- out = RawResultStruct.new
887
+ out = StringStruct.new
889
888
  code = @hegel_generate_string_fn.call(ctx, tc, generator, out)
890
889
  LibHegel.check!(self, ctx, code)
891
- out[:data].read_bytes(out[:len]).force_encoding(Encoding::UTF_8)
890
+ read_string(out)
892
891
  ensure
893
- generate_string_result_free(ctx, out)
892
+ owned_string_free(ctx, out)
894
893
  end
895
894
 
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)
895
+ # No-op when +result+ is nil, matching hegel_owned_string_free's
896
+ # documented no-op-on-NULL contract (also safe on an already-freed,
897
+ # zeroed struct); not translated, for the same reason as
898
+ # #context_free. Before libhegel 0.46 this call was
899
+ # hegel_generate_string_result_free.
900
+ def owned_string_free(ctx, result)
901
901
  # Removing this call only leaks native memory; Ruby has no way to
902
902
  # observe the leak.
903
- @hegel_generate_string_result_free_fn.call(ctx, result) # mutineer:disable-line statement_removal
903
+ @hegel_owned_string_free_fn.call(ctx, result) # mutineer:disable-line statement_removal
904
904
  nil
905
905
  end
906
906
 
907
907
  # Returns drawn bytes as a String, read via +len+ the same way
908
908
  # #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.
909
+ # NUL-termination guarantee for this buffer, so the length is what
910
+ # makes the copy exact.
911
911
  #
912
912
  # Unlike #generate_string, the result is never force-encoded.
913
913
  # hegel_generate_bytes_result_t is documented as a byte buffer, not
@@ -923,7 +923,7 @@ module Hegel
923
923
  # header documents hegel_generate_bytes_result_free as safe on an
924
924
  # already-freed (zeroed) struct too.
925
925
  def generate_bytes(ctx, tc, min_size, max_size)
926
- out = RawResultStruct.new
926
+ out = StringStruct.new
927
927
  code = @hegel_generate_bytes_fn.call(ctx, tc, min_size, max_size, out)
928
928
  LibHegel.check!(self, ctx, code)
929
929
  out[:data].read_bytes(out[:len])
@@ -953,9 +953,8 @@ module Hegel
953
953
  # a NUL character in it reaches the engine instead of ending it.
954
954
  def string_generator_regex(ctx, pattern, fullmatch, alphabet = nil)
955
955
  out = FFI::MemoryPointer.new(:pointer)
956
- bytes = pattern.encode(Encoding::UTF_8)
957
956
  code = @hegel_string_generator_regex_fn.call(
958
- ctx, bytes_to_pointer(bytes), bytes.bytesize, fullmatch, alphabet, out
957
+ ctx, string_view(pattern.encode(Encoding::UTF_8)), fullmatch, alphabet, out
959
958
  )
960
959
  LibHegel.check!(self, ctx, code)
961
960
  out.read_pointer
@@ -1103,8 +1102,8 @@ module Hegel
1103
1102
  end
1104
1103
 
1105
1104
  # 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
1105
+ # #generate_integer_big's min_value/max_value and for #write_string:
1106
+ # input bytes libhegel reads from, the opposite direction of an
1108
1107
  # out-parameter buffer (which libhegel writes into, and the caller
1109
1108
  # then reads).
1110
1109
  def bytes_to_pointer(bytes)
@@ -1113,21 +1112,50 @@ module Hegel
1113
1112
  ptr
1114
1113
  end
1115
1114
 
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.
1115
+ # Writes +string+'s bytes into +struct+, a StringStruct, as a borrowed
1116
+ # hegel_string_t input. The bytes go over with their length and no
1117
+ # terminator, so an interior NUL reaches the engine. An empty string
1118
+ # gets a zero-size buffer: the header accepts any pointer for a
1119
+ # length of zero.
1120
+ #
1121
+ # FFI::Struct keeps a reference to a pointer it stores in a field, so
1122
+ # the copied buffer lives as long as +struct+ does. Measured on ffi
1123
+ # 1.17.4: after a full GC, a WeakRef to a buffer held only by a struct
1124
+ # field is still alive, and one to a buffer with no holder is not. A
1125
+ # caller keeps the struct live for the native call by passing it as an
1126
+ # argument.
1127
+ def write_string(struct, string)
1128
+ struct[:data] = bytes_to_pointer(string)
1129
+ struct[:len] = string.bytesize
1130
+ struct
1131
+ end
1132
+
1133
+ # A hegel_string_t for an ordinary input argument, passed by value.
1134
+ def string_view(string)
1135
+ write_string(StringStruct.new, string)
1136
+ end
1137
+
1138
+ # A pointer to a hegel_string_t for an optional input argument: nil
1139
+ # for an absent value, which marshals to NULL, and a struct
1140
+ # otherwise. The header separates the two cases: NULL means absent,
1141
+ # and a pointer to an empty string means present and empty. "" for
1142
+ # #settings_set_database depends on that difference.
1143
+ def optional_string(string)
1144
+ string && string_view(string)
1145
+ end
1146
+
1147
+ # Packs +names+ (an Array of Ruby Strings) into a contiguous array of
1148
+ # hegel_string_t for #new_state_machine's rule_names/invariant_names
1149
+ # arguments: one StringStruct per name, each written in place over
1150
+ # its own slot of a single buffer.
1120
1151
  #
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.
1152
+ # Returns [pointer, kept_alive]. #write_string copies each name into
1153
+ # a fresh native buffer -- nothing here depends on the original
1154
+ # +names+ Strings staying live -- and each slot's struct holds a
1155
+ # reference to that buffer. The array buffer holds only addresses,
1156
+ # not references, so the caller keeping +kept_alive+ (the slot
1157
+ # structs) as a live local for the length of the call is what keeps
1158
+ # the buffers each one owns from being garbage-collected first.
1131
1159
  #
1132
1160
  # Empty +names+ maps to [nil, []] -- NULL and (by the caller passing
1133
1161
  # names.size, 0) the header's own contract for invariant_names' "no
@@ -1142,10 +1170,11 @@ module Hegel
1142
1170
  # hegel_new_state_machine reads zero names either way.
1143
1171
  return [nil, []] if names.empty? # mutineer:disable-line statement_removal
1144
1172
 
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]
1173
+ array = FFI::MemoryPointer.new(StringStruct, names.size)
1174
+ slots = names.each_with_index.map do |name, i|
1175
+ write_string(StringStruct.new(array + (i * StringStruct.size)), name)
1176
+ end
1177
+ [array, slots]
1149
1178
  end
1150
1179
 
1151
1180
  # Packs +flags+ (an Array of true/false) into a const bool * buffer,
@@ -1222,26 +1251,29 @@ module Hegel
1222
1251
  [struct[:hour], struct[:minute], struct[:second], struct[:nanosecond]]
1223
1252
  end
1224
1253
 
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.
1254
+ # Reads +out+'s const hegel_string_t* out-parameter into a Ruby
1255
+ # String, or returns nil if libhegel left it NULL. Shared by
1256
+ # #run_result_error, #failure_reproduction_blob, and #failure_caveat,
1257
+ # the out-parameters the header documents as nullable, so both
1258
+ # branches only need to be exercised once between the call sites
1259
+ # rather than at each one.
1230
1260
  def nullable_out_string(out)
1231
1261
  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)
1262
+ ptr.null? ? nil : read_string(StringStruct.new(ptr))
1263
+ end
1264
+
1265
+ # Copies the +len+ bytes at +struct+'s data pointer and labels them
1266
+ # UTF-8. The header calls every string libhegel hands back valid
1267
+ # UTF-8, but FFI::Pointer#read_bytes labels what it reads ASCII-8BIT.
1268
+ # A caller who matches an error message against a pattern of their
1269
+ # own then gets Encoding::CompatibilityError rather than an answer,
1270
+ # and the engine's own messages do carry characters outside ASCII:
1271
+ # the FilterTooMuch health check writes an em dash into the sentence
1272
+ # a caller is most likely to read. The length decides the copy, not
1273
+ # the trailing NUL the header also promises, since a string can hold
1274
+ # an interior NUL.
1275
+ def read_string(struct)
1276
+ struct[:data].read_bytes(struct[:len]).force_encoding(Encoding::UTF_8)
1245
1277
  end
1246
1278
  end
1247
1279
  end
@@ -18,7 +18,7 @@ module Hegel
18
18
  # is testable without the native engine. hegel-java makes the same split,
19
19
  # between its Libhegel interface, RealLibhegel, and FakeLibhegel.
20
20
  module LibHegel
21
- # hegel_result_t, named from hegel-c/include/hegel.h's enum of the same
21
+ # hegel_result_t, named from libhegel/include/hegel.h's enum of the same
22
22
  # name. HEGEL_OK is success; every other value is a negative error code.
23
23
  HEGEL_OK = 0
24
24
  HEGEL_E_STOP_TEST = -1
@@ -118,7 +118,7 @@ module Hegel
118
118
  start_span stop_span
119
119
  new_collection collection_more collection_reject collection_free
120
120
  generate_float
121
- string_generator_text string_generator_free generate_string generate_string_result_free
121
+ string_generator_text string_generator_free generate_string owned_string_free
122
122
  generate_bytes generate_bytes_result_free
123
123
  string_generator_regex string_generator_email string_generator_url string_generator_domain
124
124
  generate_ipv4 generate_ipv6 generate_uuid
@@ -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.45.4"
8
+ LIBHEGEL_VERSION = "0.47.2"
9
9
  end
data/lib/hegel/locate.rb CHANGED
@@ -16,7 +16,7 @@ module Hegel
16
16
  # fallback.
17
17
  # 2. The copy bundled into the gem under +lib/hegel/libhegel/+.
18
18
  #
19
- # A sibling `../hegel-rust` checkout is deliberately not searched. The env
19
+ # A sibling engine checkout (`../hegel`) is deliberately not searched. The env
20
20
  # override already covers local-engine-build workflows without depending
21
21
  # on any particular checkout layout, and `rake libhegel:fetch` covers the
22
22
  # rest; a rule that depends on a layout this repo's own checkout does not
@@ -32,10 +32,10 @@ module Hegel
32
32
  LIBRARY_PATH_ENV = "HEGEL_LIBHEGEL_PATH"
33
33
 
34
34
  # Published release asset name for each supported "<host_cpu>-<host_os>"
35
- # pair, mirroring the artifacts hegel-rust publishes for the pinned
36
- # LIBHEGEL_VERSION. Hosts absent from this table (e.g. x86_64-darwin,
37
- # Intel Mac) are unsupported because hegel-rust does not publish a build
38
- # for them.
35
+ # pair, mirroring the artifacts the libhegel release of the pinned
36
+ # LIBHEGEL_VERSION publishes. Hosts absent from this table (e.g.
37
+ # x86_64-darwin, Intel Mac) are unsupported because that release does not
38
+ # publish a build for them.
39
39
  ASSET_NAMES = {
40
40
  "arm64-darwin" => "libhegel-darwin-arm64.dylib",
41
41
  "x86_64-linux" => "libhegel-linux-amd64.so",
@@ -76,7 +76,7 @@ module Hegel
76
76
  end
77
77
 
78
78
  # The release asset name for the given host, or raises Hegel::Error if
79
- # hegel-rust does not publish a build for it.
79
+ # the libhegel release does not publish a build for it.
80
80
  def asset_name(host_cpu:, host_os:)
81
81
  release_asset_name(host_cpu, host_os) || raise(Hegel::Error, unsupported_host_message(host_cpu, host_os))
82
82
  end
@@ -101,7 +101,7 @@ module Hegel
101
101
  # directory override is how an unsupported host (e.g. x86_64-darwin)
102
102
  # points at a local build in the first place, so treating it as an
103
103
  # error here would close off that path.
104
- # 2. libhegel_c.<ext>, the name cargo gives the library when hegel-c's
104
+ # 2. libhegel_c.<ext>, the name cargo gives the library when libhegel's
105
105
  # crate (`[lib] name = "hegel_c"`) is built without renaming it.
106
106
  # 3. libhegel.<ext>, a renamed copy, per the hegel-go and hegel-ocaml
107
107
  # READMEs.
@@ -122,8 +122,8 @@ module Hegel
122
122
  [release_asset_name(host_cpu, host_os), "libhegel_c.#{ext}", "libhegel.#{ext}"].compact
123
123
  end
124
124
 
125
- # The release asset name for the given host, or nil if hegel-rust does
126
- # not publish a build for it. Split from asset_name (which raises) so a
125
+ # The release asset name for the given host, or nil if the libhegel
126
+ # release does not publish a build for it. Split from asset_name (which raises) so a
127
127
  # directory override can treat an unsupported host as "skip this
128
128
  # candidate" instead of an error.
129
129
  def release_asset_name(host_cpu, host_os)
@@ -159,7 +159,7 @@ module Hegel
159
159
  end
160
160
 
161
161
  # Shared-library extension for host_os alone, independent of whether the
162
- # specific host_cpu is one hegel-rust publishes a build for. This lets
162
+ # specific host_cpu is one libhegel publishes a build for. This lets
163
163
  # an explicit directory override (step 1) resolve on an otherwise
164
164
  # unsupported host (e.g. x86_64-darwin), matching a local libhegel build
165
165
  # that always produces the OS-native extension.
data/lib/hegel/runner.rb CHANGED
@@ -178,9 +178,9 @@ module Hegel
178
178
  # Pulls test cases from +run+ until hegel_next_test_case reports none
179
179
  # left (a nil out-parameter, not an error). Never counts iterations
180
180
  # itself: test_cases bounds generation, not how many times shrinking
181
- # and rejected cases call the body. Measured against libhegel 0.45.4, a
181
+ # and rejected cases call the body. Measured against libhegel 0.47.1, a
182
182
  # run configured for 20 test cases whose body rejected nearly every case
183
- # took 475 to 1607 iterations. +stats+ does its own, different counting
183
+ # took 811 to 1847 iterations. +stats+ does its own, different counting
184
184
  # -- see GenerationStats above.
185
185
  def drive(impl, ctx, run, stats, captures, &block)
186
186
  loop do
data/lib/hegel/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hegel
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.4"
5
5
  end
@@ -13,16 +13,28 @@ require_relative "../hegel/libhegel_version"
13
13
  # needs is fetched here ahead of time, not at resolution time.
14
14
  module Hegel
15
15
  module LibhegelFetch
16
- RELEASE_BASE = "https://github.com/hegeldev/hegel-rust/releases/download"
16
+ # libhegel 0.47.2 is the first engine release from the hegeldev/hegel
17
+ # monorepo, tagged libhegel/<version>. hegeldev/hegel-rust holds the
18
+ # releases up to 0.47.1, tagged libhegel-v<version>, and gets no new
19
+ # release. hegeldev/hegel does not carry the assets of those older
20
+ # releases (measured on 2026-10-19: HTTP 404 for libhegel/0.47.1/hegel.h),
21
+ # so the version selects the repository.
22
+ MONOREPO_RELEASE_BASE = "https://github.com/hegeldev/hegel/releases/download"
23
+ MONOREPO_FIRST_VERSION = Gem::Version.new("0.47.2")
24
+ LEGACY_RELEASE_BASE = "https://github.com/hegeldev/hegel-rust/releases/download"
17
25
  DEFAULT_ROOT = File.expand_path("../../tmp/libhegel", __dir__)
18
26
 
19
27
  module_function
20
28
 
21
- # Since libhegel 0.42.1, hegel-rust tags an engine release
22
- # libhegel-v<version>. The plain v<version> tags belong to its hegeltest
23
- # crate, whose version numbers differ from the engine's.
29
+ # In hegeldev/hegel-rust, the plain v<version> tags belong to the
30
+ # hegeltest crate, whose version numbers differ from the engine's. In
31
+ # hegeldev/hegel, the frontends/rust/<version> tags have the same role.
24
32
  def release_base(version)
25
- "#{RELEASE_BASE}/libhegel-v#{version}"
33
+ if Gem::Version.new(version) >= MONOREPO_FIRST_VERSION
34
+ "#{MONOREPO_RELEASE_BASE}/libhegel/#{version}"
35
+ else
36
+ "#{LEGACY_RELEASE_BASE}/libhegel-v#{version}"
37
+ end
26
38
  end
27
39
 
28
40
  # Fetches the host's pinned asset into <root>/<version>/<asset>,
@@ -7,8 +7,8 @@ require_relative "../hegel/locate"
7
7
  require_relative "../hegel/libhegel_version"
8
8
  require_relative "../hegel/errors"
9
9
 
10
- # Packages this gem for the five platforms hegel-rust publishes a libhegel
11
- # build for (see Hegel::Locate::ASSET_NAMES), on top of the platform-
10
+ # Packages this gem for the five platforms libhegel publishes a build
11
+ # for (see Hegel::Locate::ASSET_NAMES), on top of the platform-
12
12
  # independent "ruby" gem `rake build` already produces. A platform gem
13
13
  # differs from that ruby gem in exactly two ways: spec.platform names the
14
14
  # platform, and lib/hegel/libhegel/ (Hegel::Locate::GEM_LIBHEGEL_DIR) carries
@@ -103,7 +103,7 @@ module Hegel
103
103
  end
104
104
 
105
105
  namespace :platform_gems do
106
- desc "Build the hegeltest gem for every platform hegel-rust publishes libhegel for, " \
106
+ desc "Build the hegeltest gem for every platform libhegel publishes a build for, " \
107
107
  "from assets already fetched by `rake libhegel:fetch_all`"
108
108
  task :build do
109
109
  Hegel::PlatformGems.build_all.each { |path| puts "built: #{path}" }
data/sig/hegel.rbs CHANGED
@@ -544,7 +544,7 @@ module Hegel
544
544
  def string_generator_text: (untyped ctx, min_size: Integer, max_size: Integer, ?codec: String?, ?min_codepoint: Integer, ?max_codepoint: Integer) -> untyped
545
545
  def string_generator_free: (untyped ctx, untyped generator) -> nil
546
546
  def generate_string: (untyped ctx, untyped tc, untyped generator) -> String
547
- def generate_string_result_free: (untyped ctx, untyped result) -> nil
547
+ def owned_string_free: (untyped ctx, untyped result) -> nil
548
548
  def generate_bytes: (untyped ctx, untyped tc, Integer min_size, Integer max_size) -> String
549
549
  def generate_bytes_result_free: (untyped ctx, untyped result) -> nil
550
550
  def string_generator_regex: (untyped ctx, String pattern, bool fullmatch, ?untyped alphabet) -> untyped
@@ -494,12 +494,12 @@ expose) are not available in this binding; `codec:`, `min_codepoint:`, and
494
494
  `max_codepoint:` are the alphabet controls it exposes today.
495
495
 
496
496
  Other generators cover most character-set jobs. These results were measured
497
- with libhegel 0.45.4:
497
+ with libhegel 0.47.1:
498
498
 
499
499
  | Job | Generator | Measured output |
500
500
  |---|---|---|
501
- | Characters to include | `from_regex("[ぁ-んA-Z0-9]{1,5}", fullmatch: true)` | `"0"`, `"ぢ3"`, `"るJこ"`; 30 draws all matched the pattern |
502
- | Characters to exclude | `from_regex("[^,\\t\\n]{0,6}", fullmatch: true)` | `""`, `"\u0010ùóR'<"`; 30 draws contained no comma, tab, or newline |
501
+ | Characters to include | `from_regex("[ぁ-んA-Z0-9]{1,5}", fullmatch: true)` | `"0"`, `"D"`, `"Yもで"`; 30 draws all matched the pattern |
502
+ | Characters to exclude | `from_regex("[^,\\t\\n]{0,6}", fullmatch: true)` | `""`, `"RÈbÁ¡"`; 30 draws contained no comma, tab, or newline |
503
503
  | A contiguous block | `text(min_codepoint: 0x300, max_codepoint: 0x36F)` | Every character of 100 draws was in U+0300 through U+036F |
504
504
  | An arbitrary alphabet | `arrays(sampled_from(%w[ß a İ]), max_size: 4).map(&:join)` | 30 draws, each built only from `ß`, `a`, and `İ` |
505
505
 
@@ -512,14 +512,13 @@ inputs and reported this health check:
512
512
 
513
513
  ```
514
514
  FailedHealthCheck: FilterTooMuch — it looks like this test is filtering out
515
- too many inputs. 50 inputs were filtered out by assume() while only 0 valid
516
- inputs were generated. If this is expected, suppress the check with
517
- suppress_health_check = [HealthCheck::FilterTooMuch].
515
+ too many inputs. 50 inputs were rejected by assume() or a filter while only
516
+ 0 valid inputs were generated. If this is expected, suppress the
517
+ FilterTooMuch health check with the suppress_health_check setting.
518
518
  ```
519
519
 
520
- The engine writes that last sentence in its own naming. This binding takes
521
- the same setting as `suppress_health_check: [:filter_too_much]`, listed in
522
- [Settings](#settings).
520
+ This binding takes that setting as
521
+ `suppress_health_check: [:filter_too_much]`, listed in [Settings](#settings).
523
522
 
524
523
  Unconfigured `text` draws from a much wider Unicode range. In 200 draws of
525
524
  `text(min_size: 1, max_size: 4)`, the results included emoji, private-use
@@ -745,6 +744,13 @@ translated): `from_regex(/[a-z]{3}/)` raises `Hegel::Error` at draw time,
745
744
  Regexp"`. Pass `my_regexp.source` explicitly for a pattern confirmed to
746
745
  need no flags and to use only syntax the two grammars share.
747
746
 
747
+ A pattern that no string matches, such as `"(?!)"`, also raises
748
+ `Hegel::Error` at draw time. The engine writes the message, and it names
749
+ the pattern: `from_regex("(?!)")` raises
750
+ `"HEGEL_E_INVALID_ARG (-5): regex pattern \"(?!)\" matches no string the
751
+ generator can produce"`. Since libhegel 0.45.7 the engine makes this
752
+ check.
753
+
748
754
  ### `emails`
749
755
 
750
756
  An RFC 5321/5322 email address `String`.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hegeltest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - meganemura
@@ -70,6 +70,8 @@ files:
70
70
  - docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md
71
71
  - docs/adr/0023-leave-unset-settings-to-the-engines-profile.md
72
72
  - docs/adr/0024-release-when-a-merge-changes-the-gem-version.md
73
+ - docs/adr/0025-copy-each-string-input-into-a-hegel-string-t.md
74
+ - docs/adr/0026-select-the-engine-release-repository-by-version.md
73
75
  - docs/architecture.md
74
76
  - lib/hegel.rb
75
77
  - lib/hegel/draw_name.rb