Fuzz

:= []

Define typed, deterministic inputs and the pure property that roc-fuzz tests.

Most targets use target, return keep for useful inputs, and build their input generator from u8, u64, str, bytes, and list. Generators for record fields compose with Roc's .Fuzz record builder.

Input := { bytes : List(U8), radix : U8 }.{
	generator_for = |_| {
		{ bytes: Fuzz.bytes, radix: Fuzz.u8_in(2, 36) }.Fuzz
	}
}

target = Fuzz.target({
	name: "decode",
	test: |input| if input.bytes.is_empty() Fuzz.reject else Fuzz.keep,
	show: Str.inspect,
})

Start with these typed combinators. target_with, from_bytes, and the Arbitrary module support advanced or migration use cases.

keep : Outcome

Mark a generated value as useful after the property has held.

alloc_count! : () => U64

Total allocations the platform has served this process so far.

Read it before and after a region of code and subtract to count that region's allocations. Counts every roc_alloc and roc_realloc; a reallocation counts as one allocation.

The absolute value is meaningless across runs -- the counter is process-wide and the fuzzer reuses the process for millions of inputs -- so always assert on a difference, never on the raw number.

live_alloc_count! : () => U64

Allocations served but not yet freed.

A region that frees everything it allocates leaves this unchanged. Use it to assert that a property does not leak; see expect_no_leaks!.

measure_allocs! : ({} => a) => { value : a, allocations : U64 }

Run body! and report how many allocations it performed.

{ value: dict, allocations } = Fuzz.measure_allocs!(|{}| build_dict(pairs))
expect_allocs_at_most! : U64, ({} => a) => a

Crash unless body! performs at most limit allocations.

This is the allocation analogue of a property assertion: it turns a silent performance regression, such as a builtin that stops mutating a uniquely owned value in place, into a fuzz failure.

expect_allocs_at_least! : U64, ({} => a) => a

Crash unless body! performs at least limit allocations.

This is useful for copy-on-write invariants: an operation on shared storage must allocate rather than mutate an alias in place.

expect_no_leaks! : ({} => a) => {}

Crash unless body! frees everything it allocated.

The result is dropped before the balance is read, so a value that is still alive does not read as a leak.

reject : Outcome

Discard a generated value that is outside the property's valid domain.

Prefer generators that produce valid inputs directly. Use reject for preconditions that are awkward or expensive to encode in a generator.

target : { name : Str, test : a -> Outcome, show : a -> Str } -> Target where [a.generator_for : FuzzEncoding -> Generator(a)]

Build a fuzz target using the input type's statically dispatched generator.

The input type must define generator_for : FuzzEncoding -> Generator(a). For each fuzzer input, target generates one value, calls test, and uses show when a person asks the runner to render that saved input.

target_with : { name : Str, generator : Generator(a), test : a -> Outcome, show : a -> Str } -> Target

Build a target from an explicit generator.

This is useful for a one-off structural input that does not need a nominal generator_for method. Prefer target when the input type owns its generation policy.

target_with! : { name : Str, generator : Generator(a), test! : a => Outcome, show : a -> Str } -> Target

Effectful sibling of target_with.

Use this when the property itself needs the platform's allocation counters, for example to assert that a builtin mutates a uniquely owned value in place instead of reallocating.

from_bytes : { name : Str, test : List(U8) -> U8 } -> Target

Adapt an existing List(U8) -> U8 quality target.

The returned byte is ignored; a crash or failed expect still reports a failure. This is a migration bridge for existing byte-oriented targets. New targets should prefer target and typed generators.

constant : a -> Generator(a)

Generate the same value without consuming any input bytes.

map : Generator(a), (a -> b) -> Generator(b)

Transform the value produced by a generator while preserving its state.

map2 : Generator(a), Generator(b), (a, b -> c) -> Generator(c)

Generate two values in sequence and combine them.

Roc's { first: gen_a, second: gen_b }.Fuzz record-builder syntax lowers to this combinator and chains it for larger records. Calling map2 directly is useful for tuples and custom constructors.

u8 : Generator(U8)

Generate any U8 value.

u8_in : U8, U8 -> Generator(U8)

Generate a U8 in the inclusive range from low through high.

The generator crashes if low is greater than high.

u64 : Generator(U64)

Generate any U64 value.

u64_in : U64, U64 -> Generator(U64)

Generate a U64 in the inclusive range from low through high.

The generator crashes if low is greater than high.

f32 : Generator(F32)

Generate an F32, biased toward the values that break float code.

About half of all generated values come from a fixed table of boundary values: quiet, negative, and signalling NaN; positive and negative infinity; positive and negative zero; the smallest and largest subnormals; the smallest normal; 1.0 and -1.0; the machine epsilon; F32.highest and F32.lowest; and 2^24, the first magnitude at which consecutive integers stop being representable. The other half are arbitrary IEEE 754 bit patterns, which are almost always huge or tiny magnitudes.

The generator always consumes five bytes, so composing it into a record does not shift the input bytes that later fields decode. Exhausted input produces 0.0. Use f32_in when the target needs a finite value within known bounds.

f32_in : F32, F32 -> Generator(F32)

Generate a finite F32 in the inclusive range from low through high.

Both endpoints are reachable. Unlike f32, this generator never produces NaN or an infinity. It crashes if either bound is not finite or if low is greater than high.

f64 : Generator(F64)

Generate an F64, biased toward the values that break float code.

About half of all generated values come from a fixed table of boundary values: quiet, negative, and signalling NaN; positive and negative infinity; positive and negative zero; the smallest and largest subnormals; the smallest normal; 1.0 and -1.0; the machine epsilon; F64.highest and F64.lowest; and 2^53, the first magnitude at which consecutive integers stop being representable. The other half are arbitrary IEEE 754 bit patterns, which are almost always huge or tiny magnitudes.

The generator always consumes nine bytes, so composing it into a record does not shift the input bytes that later fields decode. Exhausted input produces 0.0. Use f64_in when the target needs a finite value within known bounds.

f64_in : F64, F64 -> Generator(F64)

Generate a finite F64 in the inclusive range from low through high.

Both endpoints are reachable. Unlike f64, this generator never produces NaN or an infinity. It crashes if either bound is not finite or if low is greater than high.

bytes : Generator(List(U8))

Generate a byte list with a length and allocation shape chosen from input.

Use raw_bytes when the target must receive every fuzzer byte unchanged.

raw_bytes : Generator(List(U8))

Generate the complete fuzzer input as one byte list without decoding it.

This is mainly useful for migration and byte-format targets. Typed targets usually get better mutations and clearer properties from smaller generators composed into their real input shape.

str : Generator(Str)

Generate a valid UTF-8 Str with varied length and allocation shape.

list : Generator(a), U64 -> Generator(List(a))

Generate a list containing at most max_len values.

Lengths range from zero through max_len, inclusive. max_len limits the decoded list length; the runner's maximum raw input size is configured separately.

Outcome

:= [Keep, Reject]

Whether a generated value belongs to the useful input domain.

Return Keep after the property holds. Return Reject when a generated value does not satisfy a precondition. A crash or failed expect is a fuzz failure; Reject is not.

Generator : Arbitrary -> { value : a, state : Arbitrary }

A deterministic decoder from fuzzer bytes to a typed Roc value.

Each generator returns the generated value and the unconsumed state, so combinators can decode several values from one input. Most applications do not need to inspect Arbitrary directly.

FuzzEncoding

:= [Default]

Selects the generator used by a nominal type's generator_for method.

Default is currently the only encoding. The marker gives static dispatch the same shape as APIs such as Json.parser_for and leaves room for future generation policies.