Skip to content

libhegel reference

libhegel implements the core of property-based testing: generation, shrinking, the example database, and the decision of what to run next. It ships as a shared library (libhegel.so, libhegel.dylib, hegel.dll) with a C ABI.

  • Library: the language-specific frontend that calls into libhegel. Sometimes called the caller below, since from libhegel's point of view it is whatever is making the calls.
  • Context: holds the diagnostic message of a failed call. Passed as the first argument to nearly every function.
  • Run: the full lifecycle of one property test, including executing many test cases and shrinking any failures.
  • Test case: a single execution of the test function and the concrete values generated for it. Cloning a handle yields more handles onto the same test case, each with its own choice sequence.
  • Span: a labeled grouping of draws that tells the shrinker which draws belong to one unit.
  • Reproduce blob: a base64 string encoding a test case's choice sequence, which can be replayed later to reproduce it exactly. It is only guaranteed to reproduce the failure in the version of Hegel in which it was generated.

Every function takes a hegel_context_t* as its first argument and returns a hegel_result_t code, except for hegel_context_new, which returns a context, and hegel_context_last_error, which returns the message pointer directly.

HEGEL_OK is zero and every error code is negative. Anything else a call produces is written through a trailing out-parameter named out_*.

Every function returns HEGEL_E_INVALID_HANDLE when passed a NULL handle (except the *_free functions, where NULL is a no-op) and HEGEL_E_INVALID_ARG when passed any other invalid argument (a NULL out-parameter, inverted bounds, a non-UTF-8 string, and so on). The functions below leave these implicit.

A NULL context is always allowed and opts out of error messages. The call still returns its usual error code. A context must not be used concurrently from multiple threads, since each fallible call overwrites the stored message.

hegel_context_new
signature
hegel_context_t *hegel_context_new(void)
returnsA new error reporting context initialized with an empty message. Never returns NULL. Must be freed with hegel_context_free.
hegel_context_free
signature
hegel_result_t hegel_context_free(hegel_context_t *ctx)
parametersctxThe context being freed. No-op when called with NULL.
returnsHEGEL_OK.
hegel_context_last_error
signature
const char *hegel_context_last_error(const hegel_context_t *ctx)
parametersctxThe context to read.
returnsThe most recent error message recorded on ctx, or the empty string if the most recent call taking ctx succeeded. NULL only if ctx is NULL. The pointer borrows the context's internal buffer and is invalidated by the next call taking the same context.
hegel_version
signature
hegel_result_t hegel_version(hegel_context_t *ctx, const char **out_version)
parametersout_versionReceives libhegel's version string, e.g. "0.14.12". The pointer is static and valid for the program's lifetime.
returnsHEGEL_OK.

Pointers you pass into a libhegel function are always owned by the caller. libhegel reads them during the call and copies whatever it needs to keep, so you may free or reuse the memory as soon as the call returns. Run results own their data and are independent of the run they came from.

Release every pointer returned by these functions with its matching free:

ConstructorDestructor
hegel_context_newhegel_context_free
hegel_settings_newhegel_settings_free
hegel_run_starthegel_run_free
hegel_test_case_from_blobhegel_test_case_free
hegel_next_test_casehegel_test_case_free
hegel_test_case_clonehegel_test_case_free
hegel_run_resulthegel_run_result_free
hegel_run_result_failurehegel_failure_free
hegel_string_generator_*hegel_string_generator_free
hegel_generate_byteshegel_generate_bytes_result_free
hegel_generate_stringhegel_generate_string_result_free

Every other pointer libhegel hands back is a borrowed string. The caller must not free it, and it is valid only until a documented point. hegel_context_last_error is invalidated by the next call on that context.

A settings handle is built up with setters, handed to hegel_run_start, and then freed. Settings can be reused across runs.

A configured handle may be shared across threads, but do not call setters concurrently on the same handle.

hegel_settings_new
signature
hegel_result_t hegel_settings_new(hegel_context_t *ctx, hegel_settings_t **out_settings)
parametersout_settingsReceives a handle initialized with libhegel's defaults: 100 test cases, all phases enabled, normal verbosity, no seed, and the default disk database under .hegel/.
returnsHEGEL_OK.
notesWhen a CI environment is detected (via CI, GITHUB_ACTIONS, and similar variables) the defaults change: the database is disabled and derandomization is enabled. Override either with the explicit setters.
hegel_settings_free
signature
hegel_result_t hegel_settings_free(hegel_context_t *ctx, hegel_settings_t *s)
parameterssThe handle to free. Safe to call with NULL.
returnsHEGEL_OK.
hegel_settings_set_test_cases
signature
hegel_result_t hegel_settings_set_test_cases(hegel_context_t *ctx, hegel_settings_t *s, uint64_t n)
parametersnMaximum number of valid test cases to run before declaring the property held. 100 by default. Cases rejected by an assumption do not count against this budget.
returnsHEGEL_OK.
hegel_settings_set_stateful_step_count
signature
hegel_result_t hegel_settings_set_stateful_step_count(hegel_context_t *ctx, hegel_settings_t *s, int64_t n)
parametersnTarget number of steps to run per stateful test case. Each stateful case runs at least one step and at most n. The default is 50. n must be at least 1.
returnsHEGEL_OK.
hegel_settings_set_mode
signature
hegel_result_t hegel_settings_set_mode(hegel_context_t *ctx, hegel_settings_t *s, uint32_t mode)
parametersmodeA full run loop or a single test case with no shrinking. See hegel_mode_t.
returnsHEGEL_OK.
notesThe enum-valued setters take uint32_t rather than the enum type so that an out-of-range value is an error instead of undefined behavior.
hegel_settings_set_seed
signature
hegel_result_t hegel_settings_set_seed(hegel_context_t *ctx, hegel_settings_t *s, uint64_t seed, bool has_seed)
parametersseedThe RNG seed to initialize generation with.
has_seedWhen false (the default), libhegel picks a fresh random seed at run start.
returnsHEGEL_OK.
hegel_settings_set_derandomize
signature
hegel_result_t hegel_settings_set_derandomize(hegel_context_t *ctx, hegel_settings_t *s, bool derandomize)
parametersderandomizeDerive the seed from a stable hash of the database key instead of fresh randomness when no explicit seed is set.
returnsHEGEL_OK.
notesUseful in CI, where you want repeated runs of one test to be deterministic but different tests to still see different inputs.
hegel_settings_set_database
signature
hegel_result_t hegel_settings_set_database(hegel_context_t *ctx, hegel_settings_t *s, const char *database)
parametersdatabaseNULL sets it to the default: ./.hegel/examples/. "" disables the database entirely. Discovered failures will not be stored. Anything else is used as the database root directory. The directory will be created if it does not already exist.
returnsHEGEL_OK.
hegel_settings_set_database_key
signature
hegel_result_t hegel_settings_set_database_key(hegel_context_t *ctx, hegel_settings_t *s, const char *key)
parameterskeyScopes stored and replayed examples. NULL clears it (the default).
returnsHEGEL_OK.
hegel_settings_set_phases
signature
hegel_result_t hegel_settings_set_phases(hegel_context_t *ctx, hegel_settings_t *s, uint32_t phases)
parametersphasesA bitwise OR of hegel_phase_t values to toggle phases. The default is HEGEL_PHASE_ALL
returnsHEGEL_OK.
hegel_settings_set_suppress_health_check
signature
hegel_result_t hegel_settings_set_suppress_health_check(hegel_context_t *ctx, hegel_settings_t *s, uint32_t checks)
parameterschecksA bitwise OR of hegel_health_check_t values naming the checks to toggle. Each call overwrites the previous suppressions.
returnsHEGEL_OK.
hegel_settings_set_report_multiple_failures
signature
hegel_result_t hegel_settings_set_report_multiple_failures(hegel_context_t *ctx, hegel_settings_t *s, bool yes)
parametersyesWhen true, libhegel keeps generating after the first failure to surface additional distinct bugs. Failures from different locations in the program are considered distinct bugs. The final result lists all of them. When false, the run stops after the first failing example.
returnsHEGEL_OK.
hegel_settings_set_verbosity
signature
hegel_result_t hegel_settings_set_verbosity(hegel_context_t *ctx, hegel_settings_t *s, uint32_t v)
parametersvControls the output verbosity. See hegel_verbosity_t.
returnsHEGEL_OK.
hegel_settings_set_backend
signature
hegel_result_t hegel_settings_set_backend(hegel_context_t *ctx, hegel_settings_t *s, uint32_t backend)
parametersbackendA hegel_backend_t value selecting the source of randomness.
returnsHEGEL_OK.
notesOnce an explicit backend has been set on a handle there is no way to change it within a run.

The caller starts a run, repeatedly asks for the next test case, reports its outcome, and reads the run result after all test cases have been run.

The run handle owns the suspended run loop as a future, and each hegel_next_test_case call resumes it on the calling thread until it returns the next test case or finishes.

hegel_run_start
signature
hegel_result_t hegel_run_start(hegel_context_t *ctx,
                               const hegel_settings_t *settings,
                               hegel_output_callback_t callback,
                               void *user_data,
                               hegel_run_t **out_run)
parameterssettingsThe settings for this run. The caller can free the settings after passing them in since libhegel copies the memory.
callbackWhere libhegel's output for this run goes. NULL leaves output on stderr.
user_dataPassed through to callback verbatim. Ignored when callback is NULL.
out_runReceives the run handle.
returnsHEGEL_OK.
notesThis only sets up the run. No test case is generated until the first hegel_next_test_case call. libhegel emits while it runs inside that call, so the callback is invoked on whichever thread makes it. Because it runs inside hegel_next_test_case, the callback must not call back into libhegel on the same run.
hegel_next_test_case
signature
hegel_result_t hegel_next_test_case(hegel_context_t *ctx,
                                    hegel_run_t *run,
                                    hegel_test_case_t **out_test_case)
parametersout_test_caseReceives a handle for the next test case, or NULL once the run is finished.
returnsHEGEL_OK, including at normal completion, where *out_test_case is NULL and you should call hegel_run_result. HEGEL_E_NOT_COMPLETE if the previous test case was not marked complete.
notesThe handle is owned by the caller and must be released with hegel_test_case_free.
hegel_run_result
signature
hegel_result_t hegel_run_result(hegel_context_t *ctx,
                                hegel_run_t *run,
                                hegel_run_result_t **out_result)
parametersout_resultReceives a caller-owned copy of the finished run's result.
returnsHEGEL_OK, or HEGEL_E_NOT_COMPLETE if the run hasn't finished yet.
notesEach call produces a copy, freed separately. It stays valid after hegel_run_free.
hegel_run_free
signature
hegel_result_t hegel_run_free(hegel_context_t *ctx, hegel_run_t *run)
parametersrunThe run to free. Safe to call with NULL.
returnsHEGEL_OK.
notesIf the caller exited its loop early, any in-flight test case is marked complete and the rest of the exploration is dropped.

A test-case handle is what a test body draws from. The caller drives it with the per-test-case primitives, concludes it with hegel_mark_complete, and releases it with hegel_test_case_free.

hegel_test_case_free
signature
hegel_result_t hegel_test_case_free(hegel_context_t *ctx, hegel_test_case_t *tc)
parameterstcAny test-case handle. Safe to call with NULL.
returnsHEGEL_OK.
notesEach handle holds one reference to the shared test case. The underlying data source is released once the last reference is gone. Each handle must be freed exactly once. A run-owned test case still needs hegel_mark_complete from one of its handles before the run can advance, so make every test case complete before freeing your last handle to it.
hegel_test_case_clone
signature
hegel_result_t hegel_test_case_clone(hegel_context_t *ctx,
                                     const hegel_test_case_t *tc,
                                     hegel_test_case_t **out_test_case)
parametersout_test_caseReceives a new handle onto an independent stream of the same test case.
returnsHEGEL_OK, HEGEL_E_CONCURRENT_USE if another thread is mid-operation on the source handle, HEGEL_E_ALREADY_COMPLETE once the test case has completed.
notesThe clone shares the test case's outcome and budgets but generates from its own choice sequence, so a clone and its source can be driven concurrently from different threads while staying deterministic under replay. Collections, pools, and state machines remain shared across all handles to the test case, but do not use shared objects from two streams since it makes tests flaky.
hegel_mark_complete
signature
hegel_result_t hegel_mark_complete(hegel_context_t *ctx,
                                   hegel_test_case_t *tc,
                                   uint32_t status,
                                   const char *origin)
parametersstatusA hegel_status_t value describing how the test case ended.
originIdentifies the origin of a failure. Used only when status is HEGEL_STATUS_INTERESTING; NULL otherwise.
returnsHEGEL_OK, or HEGEL_E_ALREADY_COMPLETE if called twice on the same handle.
notesCompletion is first-caller-wins and applies to the whole test case: the first call from any handle records the outcome, and a later call on a different handle is a safe no-op. This function never returns HEGEL_E_CONCURRENT_USE: if another thread is mid-operation on the handle it waits, then completes.

libhegel groups failures by their origin. Two failures with identical origins are the same bug and get shrunk together. Each new origin is a new bug.

A library must pass a stable value for the origin, such as the location of the failing assertion.

Every draw takes a test-case handle and writes its value through an out-parameter. All of them can return HEGEL_E_STOP_TEST, meaning libhegel has exhausted its choice budget for this test case: abort the test body and call hegel_mark_complete with HEGEL_STATUS_OVERRUN.

hegel_generate_boolean
signature
hegel_result_t hegel_generate_boolean(hegel_context_t *ctx,
                                      hegel_test_case_t *tc,
                                      double p,
                                      bool forced,
                                      bool has_forced,
                                      bool *out_value)
parameterspProbability of drawing true. Must be in [0.0, 1.0].
forced / has_forcedWhen has_forced is set, the result is forced to forced.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
hegel_generate_integer
signature
hegel_result_t hegel_generate_integer(hegel_context_t *ctx,
                                      hegel_test_case_t *tc,
                                      int64_t min_value,
                                      int64_t max_value,
                                      int64_t *out_value)
parametersmin_value / max_valueInclusive bounds. Both required.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
hegel_generate_integer_big
signature
hegel_result_t hegel_generate_integer_big(hegel_context_t *ctx,
                                          hegel_test_case_t *tc,
                                          const uint8_t *min_value,
                                          size_t min_value_len,
                                          const uint8_t *max_value,
                                          size_t max_value_len,
                                          uint8_t *out_value,
                                          size_t out_value_cap,
                                          size_t *out_value_len)
parametersmin_value / max_valueInclusive bounds as two's-complement little-endian signed byte buffers. Both required and must be non-empty.
out_valueReceives the drawn value's two's-complement little-endian bytes. libhegel sign-fills the rest of the buffer up to out_value_cap, so reading the whole buffer as a fixed-width integer also yields the drawn value with no sign extension needed.
out_value_lenReceives the value's minimal length. Passing out_value_cap >= max(min_value_len, max_value_len) always succeeds.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
notesUse this for bounds outside the int64_t range; otherwise prefer hegel_generate_integer.
hegel_generate_float
signature
hegel_result_t hegel_generate_float(hegel_context_t *ctx,
                                    hegel_test_case_t *tc,
                                    uint32_t width,
                                    double min_value,
                                    double max_value,
                                    bool allow_nan,
                                    bool allow_infinity,
                                    bool exclude_min,
                                    bool exclude_max,
                                    double smallest_nonzero_magnitude,
                                    double *out_value)
parameterswidth32 or 64. 32 bit bounds must be exactly representable as float, and finite 32 bit results are exactly representable as float.
min_value / max_valueInclusive bounds. Pass -INFINITY / INFINITY for unbounded ends.
allow_nanNaN is drawn only when this is set.
allow_infinityInfinities are drawn only when this is set and the corresponding endpoint is unbounded.
exclude_min / exclude_maxMake the corresponding bound exclusive by stepping it to the next representable value at the requested width.
smallest_nonzero_magnitudeNonzero magnitudes below this are never drawn. Must be positive and finite; pass 5e-324 (width 64) or the smallest float subnormal (width 32) for no restriction.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
hegel_generate_bytes
signature
hegel_result_t hegel_generate_bytes(hegel_context_t *ctx,
                                    hegel_test_case_t *tc,
                                    uint64_t min_size,
                                    uint64_t max_size,
                                    hegel_generate_bytes_result_t *out_result)
parametersmin_size / max_sizeInclusive length bounds.
out_resultReceives a libhegel-allocated {uint8_t *data; size_t len;} the caller owns. data is never NULL after a successful draw. Release with hegel_generate_bytes_result_free.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
hegel_generate_bytes_result_free
signature
hegel_result_t hegel_generate_bytes_result_free(hegel_context_t *ctx,
                                                hegel_generate_bytes_result_t *result)
parametersresultReleased and reset to {NULL, 0}. Safe to call with NULL or an already-freed (zeroed) struct.
returnsHEGEL_OK.
notesFreeing the buffer any other way is undefined behavior.
hegel_string_generator_text
signature
hegel_result_t hegel_string_generator_text(hegel_context_t *ctx,
                                           uint64_t min_size,
                                           uint64_t max_size,
                                           const char *codec,
                                           uint32_t min_codepoint,
                                           uint32_t max_codepoint,
                                           const char *const *categories,
                                           size_t categories_len,
                                           const char *const *exclude_categories,
                                           size_t exclude_categories_len,
                                           const uint8_t *include_characters,
                                           size_t include_characters_len,
                                           const uint8_t *exclude_characters,
                                           size_t exclude_characters_len,
                                           hegel_string_generator_t **out_generator)
parametersmin_size / max_sizeInclusive length bounds, in characters.
codecThe alphabet's starting range: "ascii", "latin-1" / "iso-8859-1", or "utf-8" / NULL for Unicode.
min_codepoint / max_codepointIntersected with the codec's range. Pass 0 and UINT32_MAX for no constraint. Surrogates are always removed.
categoriesRestricts to the union of the named Unicode general categories. NULL means no restriction. A non-NULL empty list means an empty alphabet.
exclude_categoriesRemoves the named categories.
include_characters / exclude_charactersUTF-8 buffers (pointer plus byte length) of individual characters. Characters in include_characters are included first, then characters in exclude_characters are removed.
returnsHEGEL_OK, or HEGEL_E_INVALID_ARG for constraints that leave no characters while max_size > 0.
hegel_string_generator_regex
signature
hegel_result_t hegel_string_generator_regex(hegel_context_t *ctx,
                                            const char *pattern,
                                            bool fullmatch,
                                            const hegel_string_generator_t *alphabet,
                                            hegel_string_generator_t **out_generator)
parameterspatternThe pattern to match, in Python re syntax.
fullmatchWhen true, the whole string must match the pattern. Otherwise, the match may be padded on either side.
alphabetOptional (NULL for none). Must be a text generator. Its character set constrains the padding and wildcard characters.
returnsHEGEL_OK.
hegel_string_generator_email
signature
hegel_result_t hegel_string_generator_email(hegel_context_t *ctx,
                                            hegel_string_generator_t **out_generator)
returnsHEGEL_OK. Produces RFC 5321/5322 addresses like alice@example.com.
hegel_string_generator_url
signature
hegel_result_t hegel_string_generator_url(hegel_context_t *ctx,
                                          hegel_string_generator_t **out_generator)
returnsHEGEL_OK. Produces RFC 3986 http/https URLs.
hegel_string_generator_domain
signature
hegel_result_t hegel_string_generator_domain(hegel_context_t *ctx,
                                             uint64_t max_length,
                                             hegel_string_generator_t **out_generator)
parametersmax_lengthTotal length of the fully-qualified domain name, in 4..=255.
returnsHEGEL_OK, or HEGEL_E_INVALID_ARG for a max_length that leaves no eligible top-level domains.
hegel_string_generator_free
signature
hegel_result_t hegel_string_generator_free(hegel_context_t *ctx,
                                           hegel_string_generator_t *generator)
parametersgeneratorThe generator to release. Safe to call with NULL.
returnsHEGEL_OK.
notesEach generator must be freed exactly once, and only after every draw using it has completed.
hegel_generate_string
signature
hegel_result_t hegel_generate_string(hegel_context_t *ctx,
                                     hegel_test_case_t *tc,
                                     const hegel_string_generator_t *generator,
                                     hegel_generate_string_result_t *out_result)
parametersgeneratorA generator built by one of the constructors above.
out_resultReceives a libhegel-allocated {char *data; size_t len;} the caller owns. Not NUL-terminated, and it may contain interior NUL bytes since the drawn alphabet can include U+0000, so always use len. Release with hegel_generate_string_result_free.
returnsHEGEL_OK, HEGEL_E_STOP_TEST, or HEGEL_E_ASSUME when the draw rejected itself (for example an email exceeding the RFC length cap).
hegel_generate_string_result_free
signature
hegel_result_t hegel_generate_string_result_free(hegel_context_t *ctx,
                                                 hegel_generate_string_result_t *result)
parametersresultReleased and reset to {NULL, 0}. Safe to call with NULL or an already-freed (zeroed) struct.
returnsHEGEL_OK.

These draws return small fixed-shape structs:

typedef struct { int32_t year; uint8_t month; uint8_t day; } hegel_date_t;
typedef struct { uint8_t hour; uint8_t minute; uint8_t second; uint32_t microsecond; } hegel_time_t;
typedef struct { hegel_date_t date; hegel_time_t time; } hegel_datetime_t;
hegel_generate_date
signature
hegel_result_t hegel_generate_date(hegel_context_t *ctx,
                                   hegel_test_case_t *tc,
                                   hegel_date_t min_value,
                                   hegel_date_t max_value,
                                   hegel_date_t *out_value)
parametersmin_value / max_valueInclusive bounds, as proleptic Gregorian dates with year in [-999999, 999999]. Pass {1, 1, 1} and {9999, 12, 31} for the full range.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
notesShrinks toward 2000-01-01 or the nearest bound when that is out of range.
hegel_generate_time
signature
hegel_result_t hegel_generate_time(hegel_context_t *ctx,
                                   hegel_test_case_t *tc,
                                   hegel_time_t min_value,
                                   hegel_time_t max_value,
                                   hegel_time_t *out_value)
parametersmin_value / max_valueInclusive bounds. Pass all-zeros and {23, 59, 59, 999999} for the full day.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
notesShrinks toward min_value, the representable time closest to midnight.
hegel_generate_datetime
signature
hegel_result_t hegel_generate_datetime(hegel_context_t *ctx,
                                       hegel_test_case_t *tc,
                                       hegel_datetime_t min_value,
                                       hegel_datetime_t max_value,
                                       hegel_datetime_t *out_value)
parametersmin_value / max_valueInclusive bounds on a naive datetime (no timezone).
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
notesShrinks toward 2000-01-01T00:00:00 or the nearest bound when that is out of range.
hegel_generate_uuid
signature
hegel_result_t hegel_generate_uuid(hegel_context_t *ctx,
                                   hegel_test_case_t *tc,
                                   uint8_t version,
                                   bool has_version,
                                   uint8_t *out_bytes)
parametersversion / has_versionWhen has_version is set, the RFC 4122 version nibble is forced to version (0..=15, conventionally 1..=5) and the variant nibble to the RFC 4122 variant. Without a version the 128 bits are uniform, except that the nil UUID is never produced.
out_bytesReceives 16 big-endian bytes.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
hegel_generate_ipv4
signature
hegel_result_t hegel_generate_ipv4(hegel_context_t *ctx, hegel_test_case_t *tc, uint8_t *out_bytes)
parametersout_bytesReceives the address's 4 network-order bytes.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.
hegel_generate_ipv6
signature
hegel_result_t hegel_generate_ipv6(hegel_context_t *ctx, hegel_test_case_t *tc, uint8_t *out_bytes)
parametersout_bytesReceives the address's 16 network-order bytes.
returnsHEGEL_OK or HEGEL_E_STOP_TEST.

These primitives tell libhegel how the values from individual draws are grouped, so the shrinker can make smarter decisions.

A span groups a set of draws so the shrinker can treat them as a unit. Libraries should wrap each compound generator in a span.

hegel_start_span
signature
hegel_result_t hegel_start_span(hegel_context_t *ctx, hegel_test_case_t *tc, uint64_t label)
parameterslabelIdentifies what kind of structure this span groups. The values reserved by libhegel are the hegel_label_t constants in hegel.h. Libraries may use any stable u64 to define their own spans.
returnsHEGEL_OK.
notesPair with exactly one hegel_stop_span call.
hegel_stop_span
signature
hegel_result_t hegel_stop_span(hegel_context_t *ctx, hegel_test_case_t *tc, bool discard)
parametersdiscardPass true to mark the span rejected (e.g. a filter predicate didn't hold) so libhegel retries from before the span opened.
returnsHEGEL_OK.
notesCloses the most recently opened span.

For variable-length values, libhegel decides how many elements to produce. The caller loops on hegel_collection_more, drawing one element per returned true.

hegel_new_collection
signature
hegel_result_t hegel_new_collection(hegel_context_t *ctx,
                                    hegel_test_case_t *tc,
                                    uint64_t min_size,
                                    uint64_t max_size,
                                    int64_t *out_collection_id)
parametersmin_size / max_sizeInclusive size bounds. Pass UINT64_MAX as max_size for no upper bound.
out_collection_idReceives an id to pass to the calls below.
returnsHEGEL_OK.
hegel_collection_more
signature
hegel_result_t hegel_collection_more(hegel_context_t *ctx,
                                     hegel_test_case_t *tc,
                                     int64_t collection_id,
                                     bool *out_more)
parametersout_moreReceives whether libhegel wants another element. Call in a loop until it is false and draw the next element in each loop iteration.
returnsHEGEL_OK.
hegel_collection_reject
signature
hegel_result_t hegel_collection_reject(hegel_context_t *ctx,
                                       hegel_test_case_t *tc,
                                       int64_t collection_id,
                                       const char *why)
parameterswhyOptional human-readable rejection reason (NULL is allowed). Validated but currently unused, reserved for future rejection diagnostics.
returnsHEGEL_OK.
notesTells libhegel the last element it produced is invalid.

A pool tracks a set of variable ids libhegel can draw from and shrink over. It is mostly used for stateful testing, where a rule needs to act on some previously generated value. The caller keeps its own mapping from variable id to the value it generated.

hegel_new_pool
signature
hegel_result_t hegel_new_pool(hegel_context_t *ctx, hegel_test_case_t *tc, int64_t *out_pool_id)
parametersout_pool_idReceives a pool id valid for this test case.
returnsHEGEL_OK.
hegel_pool_add
signature
hegel_result_t hegel_pool_add(hegel_context_t *ctx,
                              hegel_test_case_t *tc,
                              int64_t pool_id,
                              int64_t *out_variable_id)
parametersout_variable_idReceives a fresh variable id, which the caller associates with the value it just generated.
returnsHEGEL_OK.
hegel_pool_generate
signature
hegel_result_t hegel_pool_generate(hegel_context_t *ctx,
                                   hegel_test_case_t *tc,
                                   int64_t pool_id,
                                   bool consume,
                                   int64_t *out_variable_id)
parametersconsumeWhen true the drawn variable is removed from the pool. When false it is not removed.
out_variable_idReceives the variable id libhegel chose.
returnsHEGEL_OK, or HEGEL_E_ASSUME if the pool has no variables.

For stateful testing libhegel picks which rule runs next and the caller runs it. Each test case enables a random subset of rules and selection draws only from that subset.

hegel_new_state_machine
signature
hegel_result_t hegel_new_state_machine(hegel_context_t *ctx,
                                       hegel_test_case_t *tc,
                                       const char *const *rule_names,
                                       size_t num_rules,
                                       const char *const *invariant_names,
                                       size_t num_invariants,
                                       int64_t *out_state_machine_id)
parametersrule_names / num_rulesNUL-terminated UTF-8 names, one per rule. Must be non-empty.
invariant_names / num_invariantsNUL-terminated UTF-8 names, one per invariant.
out_state_machine_idReceives an id, valid for this test case.
returnsHEGEL_OK.
hegel_state_machine_next_rule
signature
hegel_result_t hegel_state_machine_next_rule(hegel_context_t *ctx,
                                             hegel_test_case_t *tc,
                                             int64_t state_machine_id,
                                             int64_t *out_rule_index)
parametersout_rule_indexReceives the index of the next rule to run, in [0, num_rules). HEGEL_STATE_MACHINE_DONE (-1) means libhegel's step budget for this test case is exhausted, so stop running rules.
returnsHEGEL_OK, or HEGEL_E_STOP_TEST when libhegel's choice budget is exhausted.
hegel_target
signature
hegel_result_t hegel_target(hegel_context_t *ctx,
                            hegel_test_case_t *tc,
                            double value,
                            const char *label)
parametersvalueA numeric observation. Must be finite. Higher is "more interesting." libhegel biases later test cases toward inputs that produced higher observations under the same label.
labelNon-NULL, valid UTF-8. Each label may be recorded at most once per test case.
returnsHEGEL_OK.
notesHas no effect unless HEGEL_PHASE_TARGET is enabled.

A run result is the outcome of a finished run, returned as a caller-owned copy. It stays valid after hegel_run_free, and is released separately.

A failed run produced counterexamples to the property. An errored run produced no verdict on the property at all, so it has no failures to inspect. A run errors on a failed health check, a nondeterministic test, or a panic inside libhegel.

hegel_run_result_status
signature
hegel_result_t hegel_run_result_status(hegel_context_t *ctx,
                                       const hegel_run_result_t *r,
                                       hegel_run_status_t *out_status)
parametersout_statusReceives HEGEL_RUN_STATUS_PASSED, HEGEL_RUN_STATUS_FAILED, or HEGEL_RUN_STATUS_ERROR.
returnsHEGEL_OK.
hegel_run_result_error
signature
hegel_result_t hegel_run_result_error(hegel_context_t *ctx,
                                      const hegel_run_result_t *r,
                                      const char **out_error)
parametersout_errorReceives the run-level error message when the run errored, or NULL when it completed normally. Owned by the run result and valid until hegel_run_result_free.
returnsHEGEL_OK.
hegel_run_result_failure_count
signature
hegel_result_t hegel_run_result_failure_count(hegel_context_t *ctx,
                                              const hegel_run_result_t *r,
                                              size_t *out_count)
parametersout_countReceives the number of distinct failures, by origin, that the run surfaced.
returnsHEGEL_OK.
hegel_run_result_failure
signature
hegel_result_t hegel_run_result_failure(hegel_context_t *ctx,
                                        const hegel_run_result_t *r,
                                        size_t index,
                                        hegel_failure_t **out_failure)
parametersindex0-based; must be less than the failure count.
out_failureReceives a caller-owned copy of the failure.
returnsHEGEL_OK.
hegel_failure_origin
signature
hegel_result_t hegel_failure_origin(hegel_context_t *ctx,
                                    const hegel_failure_t *f,
                                    const char **out_origin)
parametersout_originReceives the origin string the shrinker grouped this bug's probes under. Valid until hegel_failure_free.
returnsHEGEL_OK.
hegel_failure_reproduction_blob
signature
hegel_result_t hegel_failure_reproduction_blob(hegel_context_t *ctx,
                                               const hegel_failure_t *f,
                                               const char **out_blob)
parametersout_blobReceives a base64 reproduce blob encoding the minimal counterexample's choice sequence, or NULL if libhegel produced none for this failure. Valid until hegel_failure_free.
returnsHEGEL_OK.
hegel_run_result_free
signature
hegel_result_t hegel_run_result_free(hegel_context_t *ctx, hegel_run_result_t *r)
parametersrThe run result to free and the strings read off it. Safe to call with NULL.
returnsHEGEL_OK.
notesMust be called exactly once per run result.
hegel_failure_free
signature
hegel_result_t hegel_failure_free(hegel_context_t *ctx, hegel_failure_t *f)
parametersfThe failure to free and the strings read off it. Safe to call with NULL.
returnsHEGEL_OK.

A library uses a reproduce blob to replay of a counterexample. It reruns the minimal failing test case so it can display the drawn values and re-raise the test's own failure.

There is no run handle and no run loop involved. The caller drives the returned test case with the usual per-test-case primitives, concludes it with hegel_mark_complete, and decides for itself whether the blob reproduced the failure (the property failed again) or is stale/flaky (it passed).

hegel_test_case_from_blob
signature
hegel_result_t hegel_test_case_from_blob(hegel_context_t *ctx,
                                         const hegel_settings_t *s,
                                         const char *blob,
                                         hegel_output_callback_t callback,
                                         void *user_data,
                                         hegel_test_case_t **out_test_case)
parametersblobA base64 blob from hegel_failure_reproduction_blob.
callback / user_dataWhere this replay's output goes, with the same contract as hegel_run_start. The callback is only ever invoked on this thread and need not outlive the call.
out_test_caseReceives a caller-owned test-case handle. Released like any other with hegel_test_case_free.
returnsHEGEL_OK, or HEGEL_E_INVALID_ARG for a blob that is not valid (corrupt, non-UTF-8, or from an incompatible Hegel version).
notesA blob whose choices no longer match the caller's generators returns HEGEL_E_STOP_TEST from the draw that overruns.

Each kind of handle has its own threading contract:

  • A context must not be used concurrently from multiple threads. Each fallible call overwrites its stored message, so sharing one across threads is a data race.
  • A settings handle may be shared across threads once configured, but each setter call requires exclusive access.
  • A run handle must only be used from one thread at a time. Calling hegel_next_test_case, hegel_run_result, or hegel_run_free concurrently on the same run is undefined behavior.
  • A test-case handle may be driven by at most one thread at a time. Concurrent operations on it return HEGEL_E_CONCURRENT_USE. To generate from several threads, hegel_test_case_clone the handle and give each thread its own clone.
HEGEL_OK0Success.
HEGEL_E_STOP_TEST-1libhegel has exhausted its choice budget for this test case and wants the caller to abort the body and return.
HEGEL_E_ASSUME-2An assume / reject precondition failed. The current test case is invalid and should be discarded.
HEGEL_E_BACKEND-3The underlying backend reported an error. See hegel_context_last_error.
HEGEL_E_INVALID_HANDLE-4A handle pointer was NULL where it must be non-NULL.
HEGEL_E_INVALID_ARG-5An argument other than a handle was invalid.
HEGEL_E_ALREADY_COMPLETE-6hegel_mark_complete (or a primitive on the same handle) was called for a test case that has already been completed.
HEGEL_E_NOT_COMPLETE-7Something was read before it was ready: hegel_next_test_case without first completing the previous test case, or hegel_run_result before the run finished.
HEGEL_E_INTERNAL-8An internal invariant failed inside libhegel. Should not happen in practice. Please file a bug.
HEGEL_E_CONCURRENT_USE-9A single test-case handle was used from two threads at once. Clone the handle instead.

Passed to hegel_mark_complete.

HEGEL_STATUS_VALID0The test body ran to completion without issues.
HEGEL_STATUS_INVALID1An assumption was violated in this test case.
HEGEL_STATUS_OVERRUN2libhegel ran out of choice budget mid test case, typically because a draw returned HEGEL_E_STOP_TEST. Treat the case as inconclusive.
HEGEL_STATUS_INTERESTING3The property failed and this test case is a counterexample.
HEGEL_RUN_STATUS_PASSED0The property held across every generated test case.
HEGEL_RUN_STATUS_FAILED1The property failed. Inspect each distinct counterexample.
HEGEL_RUN_STATUS_ERROR2The run itself failed and produced no verdict on the property. There are no failures to inspect; read the message with hegel_run_result_error.

A bitwise OR of these is passed to hegel_settings_set_phases. The default is HEGEL_PHASE_ALL. Turn a phase off for debugging or replay tooling.

HEGEL_PHASE_EXPLICIT1 << 0Run hard-coded explicit examples (none today, reserved for future use).
HEGEL_PHASE_REUSE1 << 1Replay counterexamples persisted from previous runs. If a database path and database key aren't passed, this phase is a no-op.
HEGEL_PHASE_GENERATE1 << 2Randomly generate fresh test cases up to the test_cases budget.
HEGEL_PHASE_TARGET1 << 3Apply hill-climbing toward observed hegel_target scores between generation rounds.
HEGEL_PHASE_SHRINK1 << 4Shrink discovered failing examples.
HEGEL_PHASE_ALL31All five phases enabled. The default.

A bitwise OR of these is passed to hegel_settings_set_suppress_health_check. The default is all enabled.

HEGEL_HC_FILTER_TOO_MUCH1 << 0Aborts the run if too many draws are rejected by assumptions.
HEGEL_HC_TOO_SLOW1 << 1Aborts the run if individual test cases take too long.
HEGEL_HC_TEST_CASES_TOO_LARGE1 << 2Aborts the run if generated values are too large.
HEGEL_HC_LARGE_INITIAL_TEST_CASE1 << 3Warns if the first generated test case is already disproportionately large.
HEGEL_MODE_TEST_RUN0libhegel drives a full generate / shrink / replay loop until the test-case budget or the choice tree is exhausted. The default.
HEGEL_MODE_SINGLE_TEST_CASE1libhegel produces exactly one test case and stops, with no shrinking. Useful for replaying a stored counterexample or running an exploratory probe.
HEGEL_BACKEND_AUTO0Choose automatically (the default): urandom when running inside Antithesis, otherwise the default backend.
HEGEL_BACKEND_DEFAULT1Expand a single seeded PRNG. Runs are reproducible from the seed and shrinking / replay work as usual.
HEGEL_BACKEND_URANDOM2Read fresh entropy from /dev/urandom on every draw, falling back to an OS-seeded PRNG on platforms without it. Intended for running under Antithesis, whose fuzzer controls /dev/urandom; you almost certainly don't want it otherwise.
HEGEL_VERBOSITY_QUIET0Nothing besides the final result.
HEGEL_VERBOSITY_NORMAL1A short summary line per run. The default.
HEGEL_VERBOSITY_VERBOSE2Per-test-case progress and drawn values, plus panic diagnostics as they happen.
HEGEL_VERBOSITY_DEBUG3As verbose, plus shrinker trace output.

Passed to hegel_start_span. libhegel opens spans around its own draws. If your Hegel library opens spans, give them labels libhegel has not reserved, or shrinking may get slower.