Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quickcheck

Property testing: run a boolean property over many generated inputs and report the first counterexample, deterministically.

A Gen(a) is a seeded, sized function producing an a. It draws its randomness from the ambient Random effect, and the runner discharges that effect with a seeded SplitMix64 handler, so a whole run is a pure function of its seed. There is no real IO here: quickcheck performs no prim_rand, only the seeded stream, so a property that fails fails the same way on every backend and reruns identically from the reported seed. Determinism is the language’s contract, and a flaky property test would violate it.

The generator TYPE and its combinators live here; a derived Arbitrary(a) class (see Wire/deriving) supplies arbitrary : Gen(a) for user types by composing these combinators, so a derived instance plugs in with no rework.

Shrinking is intentionally omitted. The seam is the seed and size carried on a failing Outcome: a counterexample reproduces exactly via gen_at, and a future shrink : (a) -> List(a) hook would thread through check_go without changing this surface.

Types

Gen

type Gen(a) = Gen((Int) -> a ! {Random})

A seeded, sized generator of a. Apply it with gen_run; build one with the combinators below. The size bounds recursive shapes (list length, tree depth); make it explicit so derived instances for recursive types stay finite.

Config

type Config = Cfg { seed: U64, count: Int, max_size: Int }

How a property run configures the seeded stream: the base seed, the number of cases to try (count), and the largest size handed to a generator (max_size).

Outcome

type Outcome(a) = Passed(Int) | Failed(a, Int, U64, Int)

The result of a property run: Passed(n) after n cases held, or Failed(value, index, seed, size) with the first counterexample and the seed and size that reproduce it via gen_at.

Functions and Values

run_seeded

run_seeded : forall e0 a. (U64, () -> a ! {Random, e0}) -> a ! {e0}

Run action with the Random effect served by a seeded SplitMix64 stream, so the result is a pure function of seed. This is the REPLAYABLE handler the runner installs; call it directly to reproduce a single draw off a seed.

run_seeded(1u64, \() -> gen_run(gen_int, 5))
1

gen_run

gen_run : forall a. (Quickcheck.Gen(a), Int) -> a ! {Random}

Draw one value from g at the given size, performing Random. Discharge the Random with run_seeded (or use gen_at) to run it.

run_seeded(1u64, \() -> gen_run(gen_const(9), 5))
9

gen_at

gen_at : forall a. (Quickcheck.Gen(a), U64, Int) -> a

Draw a value from g deterministically off seed at size, no IO. This is the reproduce-a-counterexample entry point.

gen_at(gen_int, 42u64, 10)
-6

gen_const

gen_const : forall a. (a) -> Quickcheck.Gen(a)

The generator that ignores size and randomness and always yields x.

gen_at(gen_const(7), 1u64, 5)
7

gen_map

gen_map : forall a b. ((a) -> b, Quickcheck.Gen(a)) -> Quickcheck.Gen(b)

Map f over every value a generator produces.

gen_at(gen_map(\(x) -> x + 1, gen_const(4)), 1u64, 5)
5

gen_map2

gen_map2 : forall a b c. ((a, b) -> c, Quickcheck.Gen(a), Quickcheck.Gen(b)) -> Quickcheck.Gen(c)

Combine two generators with f, drawing both at the same size.

gen_at(gen_map2(\(a, b) -> a + b, gen_const(2), gen_const(3)), 1u64, 5)
5

gen_bind

gen_bind : forall a b. (Quickcheck.Gen(a), (a) -> Quickcheck.Gen(b)) -> Quickcheck.Gen(b)

Monadic bind: draw an a, then draw from the generator f picks for it.

gen_at(gen_bind(gen_const(3), \(x) -> gen_const(x * 2)), 1u64, 5)
6

gen_sized

gen_sized : forall a. ((Int) -> Quickcheck.Gen(a)) -> Quickcheck.Gen(a)

Build a generator that sees the current size, for recursive shapes that branch on remaining fuel.

gen_at(gen_sized(\(sz) -> gen_const(sz)), 1u64, 12)
12

gen_resize

gen_resize : forall a. (Int, Quickcheck.Gen(a)) -> Quickcheck.Gen(a)

Run g at a fixed size, ignoring the ambient one (shrink a recursive position by resizing it smaller).

gen_at(gen_resize(4, gen_sized(\(sz) -> gen_const(sz))), 1u64, 99)
4

gen_choose

gen_choose : forall a. (Quickcheck.Gen(a), List(Quickcheck.Gen(a))) -> Quickcheck.Gen(a)

Pick one of g0/rest uniformly, then draw from it (one arm per constructor, the shape a derived sum-type instance uses).

gen_at(gen_choose(gen_const(1), [gen_const(2)]), 5u64, 3)
1

gen_one_of

gen_one_of : forall a. (a, List(a)) -> Quickcheck.Gen(a)

Pick one of the given values uniformly (x0 or one of rest).

gen_at(gen_one_of(10, [20, 30]), 5u64, 3)
30

gen_int

gen_int : Quickcheck.Gen(Int)

Generator of Int, biased toward the edge cases (0, 1, -1, and a full-width draw) alongside small readable values.

gen_at(gen_int, 42u64, 10)
-6

gen_i64

gen_i64 : Quickcheck.Gen(I64)

Generator of I64, reusing the Int distribution.

gen_u64

gen_u64 : Quickcheck.Gen(U64)

Generator of U64, reusing the Int distribution (negatives wrap).

gen_bool

gen_bool : Quickcheck.Gen(Bool)

Generator of Bool.

gen_at(gen_bool, 7u64, 3)
false

gen_float

gen_float : Quickcheck.Gen(Float)

Generator of Float, including the nasty values (+/-0, +/-inf, NaN).

gen_char

gen_char : Quickcheck.Gen(Char)

Generator of a printable-ASCII Char.

gen_string

gen_string : Quickcheck.Gen(String)

Generator of a printable-ASCII String, length bounded by size.

gen_list

gen_list : forall a. (Quickcheck.Gen(a)) -> Quickcheck.Gen(List(a))

Generator of a List(a) whose length is bounded by size.

gen_at(gen_list(gen_const(1)), 7u64, 4)
[1, 1]

gen_option

gen_option : forall a. (Quickcheck.Gen(a)) -> Quickcheck.Gen(Option(a))

Generator of Option(a): None a quarter of the time, else Some.

gen_at(gen_option(gen_const(5)), 3u64, 3)
Some(5)

gen_pair

gen_pair : forall a b. (Quickcheck.Gen(a), Quickcheck.Gen(b)) -> Quickcheck.Gen((a, b))

Generator of a pair, both drawn at the same size.

gen_at(gen_pair(gen_const(1), gen_const(2)), 1u64, 3)
(1, 2)

gen_triple

gen_triple : forall a b c. (Quickcheck.Gen(a), Quickcheck.Gen(b), Quickcheck.Gen(c)) -> Quickcheck.Gen((a, b, c))

Generator of a triple, all drawn at the same size.

gen_at(gen_triple(gen_const(1), gen_const(2), gen_const(3)), 1u64, 3)
(1, 2, 3)

default_config

default_config : Quickcheck.Config

The default configuration: a fixed base seed, 100 cases, sizes up to 20.

default_config.count
100

check_with

check_with : forall a. (Quickcheck.Config, Quickcheck.Gen(a), (a) -> Bool) -> Quickcheck.Outcome(a)

Run prop over cfg.count inputs from gen, returning the first counterexample or the count that passed. Deterministic in cfg.seed.

check_with(default_config, gen_const(2), \(x) -> x == 2)
Quickcheck.Passed(100)

quickcheck

quickcheck : forall a. (Quickcheck.Gen(a), (a) -> Bool) -> Quickcheck.Outcome(a)

Run prop over the default configuration.

quickcheck(gen_int, \(x) -> x + 0 == x)
Quickcheck.Passed(100)

passed

passed : forall a. (Quickcheck.Outcome(a)) -> Bool

True when a run found no counterexample.

passed(quickcheck(gen_int, \(x) -> x + 1 > x))
true

show_outcome

show_outcome : forall a. (String, Quickcheck.Outcome(a)) -> String

Render an outcome for name as one report line (pass) or a block naming the counterexample and the seed and size that reproduce it.

show_outcome("positivity", Failed(0, 12, 42u64, 5))
positivity: FAILED after 12 tests.
  counterexample: 0
  reproduce: seed 42 size 5