Gen
Gen module: Hedgehog-style property-test value generators with integrated shrinking.
Shrinking is NOT a separate step bolted onto generation. Each generator produces a rose tree (GenTree(a)): a value plus a lazy list of smaller candidate values. When a property fails, the runner walks the tree toward a minimal counterexample — no bespoke shrink function required.
Because shrinking is embedded in the tree, combinators like bind / map automatically propagate shrinking correctly through dependent generators.
Typical use (see Check.all in check.march):
Check.all(Gen.list(Gen.int(-100, 100)), fn xs ->
List.reverse(List.reverse(xs)) == xs
)All generators are pure: they consume a GenRng and a size hint, and return a shrink tree plus the advanced RNG.
Types
Functions
Printable ASCII character in [32, 126]. Shrinks toward 'a' (97).
Monadic composition for dependent generators. Shrinking covers both the seed value and the function's result — when the seed shrinks, f is replayed with the same RNG so outputs remain deterministically linked.
Uniform Bool; shrinks True -> False (a property that holds for False is usually simpler to reason about than one that holds for True).
A generator that always produces the same value. Has no shrink candidates.
Pick a uniform element from a non-empty list. Shrinks toward the first element (which is assumed to be the "simplest" by user convention). Panics on empty input.
Rejection sampling: re-run the generator until it produces a value satisfying pred, up to 100 attempts. Shrink candidates that fail the predicate are pruned.
Use sparingly — very selective predicates can hurt performance.
Prefer `bind` + a targeted sub-generator when possible.Uniform float in [lo, hi); shrinks toward 0.0 (or the nearer bound) by halving the distance to the target. Terminates at target +/- 2^-20 to avoid unbounded shrink chains on floats.
Force a thunk. The Bool argument is a parser workaround and is ignored.
Weighted choice among (weight, gen) pairs. Weights must be non-negative and at least one must be positive.
Uniform integer in [lo, hi] inclusive, shrinking toward 0 (or lo if the range doesn't cross 0).
Uniform integer in [0, size], using the generator's size hint as the upper bound.
Build a shrink tree for an integer, shrinking toward target. Each candidate shrinks recursively toward the same target.
Generator for a list. The length is uniform in [0, size], where size is the size hint passed by the runner (grows from 0 to ~100 across runs).
Shrinking reduces both length and element values, producing genuinely
minimal counterexamples.Generator for a list of exactly n elements.
Transform a generator's output. Shrinking is inherited from the input.
Uniform choice among a non-empty list of generators.
Generator that produces None about 1/4 of the time and Some(x) from g the rest. Shrinks Some(x) toward None first, then shrinks x.
Run a generator to produce (tree, rng'). Internal.
Access the current size hint inside a generator. Use to build generators whose structure depends on the runner's size parameter (which grows from 0 to max_size across runs).
Example — lists that grow with size:
let growing = Gen.sized(fn s -> Gen.list_of_size(s, Gen.int(0, 100)))
Example — nested structures bounded by size:
let depth_bounded = Gen.sized(fn s ->
if s < 2 do Gen.constant(Leaf)
else Gen.map(Gen.int(0, 100), fn n -> Node(n))
)Generator for an ASCII string. Length uniform in [0, size]; characters drawn from printable ASCII. Shrinks by reducing length and char values.
Generator for a string whose characters are drawn from char_gen. Use this to build domain-specific string generators.
Example — hex strings:
let hex_char = Gen.element([48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 97, 98, 99, 100, 101, 102])
let hex_string = Gen.string_of(hex_char)
Example — digit strings:
let digit = Gen.int(48, 57)
let digit_string = Gen.string_of(digit)Monadic bind on trees. For each value in ta we produce a new tree via f; the resulting tree's children are the union of: (1) shrinks inherited from ta (each re-threaded through f) (2) shrinks of the f(root) sub-tree This is how integrated shrinking preserves relationships between dependent generators — shrinking the input automatically re-derives the output.
Force one level of shrink candidates.
Filter a tree: keep only nodes whose root satisfies pred. Children that fail the predicate are replaced with their own filtered children, so we don't lose shrinkability by pruning an intermediate layer. Returns None if the root itself fails the predicate.
Map a pure function over every node in the tree. The shape of the tree (which shrinks are tried, in what order) is preserved; only the values change.
Generator for a 2-tuple. Shrinks each component independently.