hanki

arbitrary

stdlib/core/arbitrary.hk: the Arbitrary trait and its built-in instances.

Arbitrary is the bridge between a type and the property-testing generator core (§ extra gen): a type is Arbitrary when it can describe how to draw a value of itself as a Gen<Self>. The runner samples each typed test parameter through its instance, and @derive(Arbitrary) composes a struct's / sum's instance from its fields'.

Dispatch is static and type-directed, as Decode's is (core decode): i32.arbitrary() selects the i32 instance; a bounded T: Arbitrary calls T.arbitrary(). Generation is pure: the Gen threads a seeded source.

Covered here: bool, the fixed-width integers, the arbitrary-precision int (i64-range magnitudes for now), bytes, string (full Unicode), decimal, rational, and the Option / List / Map / Entry composites: every builtin type is Arbitrary.

@derive(Arbitrary) composes a type's instance from its parts'. It supports structs (including self-recursive ones, through Option/List) and sums, generic and self-recursive ones included, as well as mutual/indirect recursion across distinct types: both derive leaf-biased sized generation (Gen.sized / Gen.one_of / Gen.recur), and generation therefore terminates even where a type refers back to itself directly or through another type. A recursive field shrinks the size budget each level (recur). For mutual recursion the deriver follows the module's type-reference edges to shrink every field that reaches the type, and the budget therefore halves around the cycle. The size-aware Option and List instances below bottom out, at None and the empty list, at the size floor, and the recursion has a base case. A generic type's params are each bounded by Arbitrary, and a bound-param part draws through T.arbitrary().

Arbitrary

trait Arbitrary

arbitrary

def arbitrary() -> Gen<Self>

A generator that draws an arbitrary value of Self. @no-doctest: produces a Gen, a generator and no concrete value; the module's test pack exercises it

impl Arbitrary<bool>

arbitrary

def arbitrary() -> Gen<bool>

Draws true or false with even odds, one uint64 per sample.

@no-doctest: draws with even odds, and odds are not a thing one sample can show

impl Arbitrary<u64>

arbitrary

def arbitrary() -> Gen<u64>

Draws the full unsigned 64-bit range, uniformly.

@no-doctest: draws the full u64 range; the type is the whole statement, and a sample would only pin the PRNG

impl Arbitrary<u32>

arbitrary

def arbitrary() -> Gen<u32>

Draws the full u32 range by truncating one uint64.

@no-doctest: draws the full u32 range; see Arbitrary<u64>

impl Arbitrary<u16>

arbitrary

def arbitrary() -> Gen<u16>

Draws the full u16 range by truncating one uint64.

@no-doctest: draws the full u16 range; see Arbitrary<u64>

impl Arbitrary<u8>

arbitrary

def arbitrary() -> Gen<u8>

Draws the full u8 range by truncating one uint64.

@no-doctest: draws the full u8 range; see Arbitrary<u64>

impl Arbitrary<i64>

arbitrary

def arbitrary() -> Gen<i64>

Draws the full signed 64-bit range, uniformly.

@no-doctest: draws the full i64 range; see Arbitrary<u64>

impl Arbitrary<i32>

arbitrary

def arbitrary() -> Gen<i32>

Draws the full i32 range by truncating one uint64.

@no-doctest: draws the full i32 range; see Arbitrary<u64>

impl Arbitrary<i16>

arbitrary

def arbitrary() -> Gen<i16>

Draws the full i16 range by truncating one uint64.

@no-doctest: draws the full i16 range; see Arbitrary<u64>

impl Arbitrary<i8>

arbitrary

def arbitrary() -> Gen<i8>

Draws the full i8 range by truncating one uint64.

@no-doctest: draws the full i8 range; see Arbitrary<u64>

impl Arbitrary<int>

arbitrary

def arbitrary() -> Gen<int>

Draws an arbitrary-precision int across the signed 64-bit range.

Magnitudes wider than i64 are still a follow-up, and a draw fits there:

v = int.arbitrary().run(seeded_source(1u64)).value.unwrap_or(0)
v.try_to_i64() == None => false

impl Arbitrary<f64>

arbitrary

def arbitrary() -> Gen<f64>

A finite f64: whole numbers and readable fractions most of the time, the IEEE boundary values often enough to hit them, and full bit-pattern coverage for the rest.

NaN and the infinities are not drawn; see any_f64, which adds them. A property comparing a drawn float to itself is the first thing anybody writes, and NaN breaks reflexivity, and a default that produced it would teach that generated-value testing is fiddly in place of that NaN is special. Ask for them and you get them; the asking is what marks the intent.

Never NaN, and a first property's reflexivity is therefore safe:

v = f64.arbitrary().run(seeded_source(1u64)).value.unwrap_or(0.0f64)
v == v => true

finite_f64

def finite_f64() -> Gen<f64>

Every finite f64, in the mixture Arbitrary<f64> draws. Named because a property that wants floats but not specials should not have to spell the mixture out.

The weights are a tuning choice and no law: they follow the practical art (Hypothesis, proptest) in spending most draws on values a human can read and the rest on coverage. Change them if a real property wants a different balance. @no-doctest: draws from a choice source; the value depends on the seed

any_f64

def any_f64() -> Gen<f64>

finite_f64 plus NaN, +inf and -inf, about one draw in ten.

The specials are what break float code: a comparison that assumes trichotomy, or a sum that swallows an infinity, and a property about IEEE behaviour should draw from here. Anything comparing a value to itself should not. @no-doctest: draws from a choice source; the value depends on the seed

wholef64

def _whole_f64() -> Gen<f64>

Whole numbers, magnitude first and sign second so the smallest choice is 0.0f64 and not the most negative value an int_range would start at.

fractionf64

def _fraction_f64() -> Gen<f64>

Readable fractions: a whole number over a small power of two or ten, and a shrunk counterexample therefore reads 0.25 or 1.5 and not 0.30000000000000004.

_divisor

def _divisor(d: i32) -> f64

notablef64

def _notable_f64() -> Gen<f64>

The values a float implementation gets wrong at the edges: both zeros, both units, the smallest subnormal and the largest normal. Built from bit patterns, f64 having no constants for them.

finitebits_f64

def _finite_bits_f64() -> Gen<f64>

Full bit-pattern coverage, forced finite. An all-ones exponent is what makes a pattern NaN or infinite, and the one case is therefore rewritten in place of rejected: filtering would burn the draw and, at a 1-in-2048 rate, do it often enough to matter.

forcefinite

def _force_finite(u: u64) -> u64

specialf64

def _special_f64() -> Gen<f64>

NaN and the two infinities, by bit pattern: exponent all ones, mantissa zero for the infinities and non-zero for NaN.

impl Arbitrary<f32>

arbitrary

def arbitrary() -> Gen<f32>

A finite f32, drawn the way Arbitrary<f64> draws: a mixture rather than a bit reinterpretation.

The reason is the same one, at 32 bits. For an integer the bit pattern is the magnitude, and a smaller choice shrinks toward zero; for a float the patterns just above zero are the subnormals, around 1e-45 here, and a bit draw would shrink every counterexample toward a denormal nobody can read.

NaN and the infinities are not drawn - any_f32 adds them, for the reason any_f64 exists.

Never NaN, as for f64:

v = f32.arbitrary().run(seeded_source(1u64)).value.unwrap_or(0.0f32)
v == v => true

finite_f32

def finite_f32() -> Gen<f32>

Every finite f32, in the mixture Arbitrary<f32> draws. The f64 twin is finite_f64, and the weights match it. @no-doctest: draws from a choice source; the value depends on the seed

any_f32

def any_f32() -> Gen<f32>

finite_f32 plus NaN, +inf and -inf, about one draw in ten. @no-doctest: draws from a choice source; the value depends on the seed

wholef32

def _whole_f32() -> Gen<f32>

Whole numbers, magnitude before sign so the smallest choice is 0.0f32.

f32of

def _f32_of(n: i32) -> f32

An i32 as an f32, by building the IEEE encoding directly.

No name in core converts into f32, and this is therefore no cast: the value is encoded. For a non-negative whole number below 2^24 that encoding is exact and short: the highest set bit is the exponent, and the bits under it are the mantissa, shifted up into the mantissa field. Going through a decimal string and f32.parse would also work, and is what this is not: a string round-trip per draw, in a generator.

wholebits

def _whole_bits(n: u32) -> u32

highestbit

def _highest_bit(n: u32) -> u32

The index of the highest set bit, for a non-zero n.

fractionf32

def _fraction_f32() -> Gen<f32>

Readable fractions: a whole number over a small power of two or ten.

divisorf32

def _divisor_f32(d: i32) -> f32

notablef32

def _notable_f32() -> Gen<f32>

The values a float implementation gets wrong at the edges, at 32 bits: both zeros, both units, the smallest subnormal and the largest normal.

finitebits_f32

def _finite_bits_f32() -> Gen<f32>

Full bit-pattern coverage, forced finite. f32's exponent is 8 bits at offset 23, the all-ones exponent is therefore 255, and clearing bit 23 takes it to 254, the largest finite one, leaving every other bit alone.

forcefinite_f32

def _force_finite_f32(u: u32) -> u32

specialf32

def _special_f32() -> Gen<f32>

NaN and the two infinities, by bit pattern.

impl<T: Arbitrary> Arbitrary<Option<T>>

arbitrary

def arbitrary() -> Gen<Option<T>>

Draws None about one time in four, else Some of a drawn T.

At the size floor forces the base case, which is what makes a type that recurses through it terminate:

g: Gen<Option<i32>> = Option.arbitrary()
g.run(seeded_source(1u64).with_size(0i32)).value.unwrap_or(Some(9i32)) => None

impl<T: Arbitrary> Arbitrary<List<T>>

arbitrary

def arbitrary() -> Gen<List<T>>

Draws a list of up to 16 elements, shrinking toward empty.

Empty at the size floor, and capped at 16 above it:

g: Gen<List<i32>> = List.arbitrary()
g.run(seeded_source(1u64).with_size(0i32)).value.unwrap_or(List.empty()).length => 0
g.run(seeded_source(1u64)).value.unwrap_or(List.empty()).length <= 16 => true

impl Arbitrary<bytes>

arbitrary

def arbitrary() -> Gen<bytes>

Draws a buffer of up to 16 arbitrary bytes.

Empty at the size floor, and capped at 16 above it:

bytes.arbitrary().run(seeded_source(1u64).with_size(0i32)).value.unwrap_or("x".to_bytes()).length => 0
bytes.arbitrary().run(seeded_source(1u64)).value.unwrap_or("x".to_bytes()).length <= 16 => true

impl Arbitrary<string>

arbitrary

def arbitrary() -> Gen<string>

Draws up to 16 arbitrary Unicode characters as valid UTF-8.

Empty at the size floor:

string.arbitrary().run(seeded_source(1u64).with_size(0i32)).value.unwrap_or("x") => ""

impl Arbitrary<rational>

arbitrary

def arbitrary() -> Gen<rational>

Draws an exact ratio of two int draws, with a non-zero denominator.

The denominator is built as abs(d) + 1, and it is therefore never zero and the draw is a real ratio and no sentinel:

r = rational.arbitrary().run(seeded_source(1u64)).value.unwrap_or(1.to_rational())
r.denominator > 0 => true

impl Arbitrary<decimal>

arbitrary

def arbitrary() -> Gen<decimal>

Draws a decimal from an int mantissa and a small non-negative scale.

The scale is a small non-negative draw, and fractional decimals therefore appear at all - decimal has no division to build them with:

d = decimal.arbitrary().run(seeded_source(1u64)).value.unwrap_or(0.to_decimal())
d.scale >= 0 and d.scale <= 12 => true

impl<K: Arbitrary, V: Arbitrary> Arbitrary<Entry<K, V>>

arbitrary

def arbitrary() -> Gen<Entry<K, V>>

Draws a key and a value independently and pairs them.

@no-doctest: pairs an arbitrary key with an arbitrary value, which is what the type says; the parts' own instances carry the examples

impl<K: Hash + Arbitrary, V: Arbitrary> Arbitrary<Map<K, V>>

arbitrary

def arbitrary() -> Gen<Map<K, V>>

Draws up to 16 entries; a repeated key takes the last-drawn value.

Bounded by the List draw underneath it, and at the size floor it is empty:

g: Gen<Map<string, i32>> = Map.arbitrary()
g.run(seeded_source(1u64).with_size(0i32)).value.unwrap_or(Map.empty()).length => 0