noprop 0.2.1

Property-based testing with plain functions, ordinary control flow, and no dependencies
Documentation
# Generator Design

This document explains the small set of decisions every noprop
generator has to make. It is a working reference for users writing
their own `sample_*` helpers, not a research plan or a roadmap for
future tuning. For the how-to side (composing primitives, bounded
rejection, `NonZero` recipes, floats), see
[`crate::docs::generator_authoring`].

## Support

The **support** of a generator is the set of values it can produce.
noprop generators are `fn(&mut TestCaseContext) -> T`, so the support
is whatever `T` values the body may return.

Two practical rules:

- **Say what the support is, in the docstring.** "Any `u32`", "any
  non-empty ASCII printable string up to `len` bytes", "any
  `NonZeroU32`". A generator whose support is fuzzy — undocumented
  edge cases, silently-excluded values — hides bugs when the property
  fails on an input the caller believed impossible.
- **Match the domain in the return type.** Prefer `NonZeroU32` over
  `u32` with a `!= 0` postcondition, `Vec<u8>` with a documented length
  bound over "usually short", and a small `enum` over a magic-number
  `u8`. Restricting the type restricts the support at the type system,
  so the property does not have to re-check what the generator already
  guaranteed.

## Distribution

A generator's **distribution** is *how* it samples values across the
support. noprop's primitives are uniform over their support:
`sample_u32` covers `0..=u32::MAX` uniformly, `sample_usize_in(ctx,
0..=n)` covers `0..=n` uniformly (through bias-free bounded rejection),
and `sample_choice(ctx, slice)` picks one slice element uniformly.

The bounded `_in` samplers share that bias-free core but differ in
result type. `sample_usize_in` returns `usize`, so `sample_u64_in`
exists for ranges that are inexpressible on 32-bit targets and would
be a cast on 64-bit ones. For a narrower integer field the sampler
whose name matches the field's type is the right choice —
`sample_u8_in`, `sample_u16_in`, `sample_u32_in`, `sample_i8_in`,
`sample_i16_in`, `sample_i32_in` — so the field is filled without an
`as` cast, and a bound that does not fit the result type is a compile
error instead of a silent truncation. The signed variants share the
same unsigned-offset technique internally. `u128` would need 128-bit
rejection sampling without a demonstrated demand, so it is not
provided.

Two reasons to move away from uniform:

- **Bias toward interesting inputs.** Domain boundaries (0, one, the
  buffer size, `u16::MAX`) are hit by uniform sampling with vanishing
  probability. Use [`sample_with_boundaries`]crate::sample_with_boundaries
  to mix a small set of interesting candidates in at an exact rational
  probability (`Ratio::one_nth(10)`, `Ratio::one_nth(100)`, …). The
  underlying uniform draw is unchanged; only the mixing weight is new.
- **Weighted branch selection.** A `match` on
  [`sample_weighted_index`]crate::sample_weighted_index picks a
  branch by a caller-specified weight vector, so rare branches can be
  exercised without paying uniformly for every one. Prefer this over
  ad-hoc `if sample_ratio(...)` chains: the weights are visible in one
  place and the RNG cost is a single draw.

Every distribution decision is exact rational, not floating point:
`Ratio::new(1, 3)` is one-in-three, not `0.333…`. This keeps the
sampled distribution identical across platforms and reproducible from a
seed.

## Termination

Every generator noprop provides terminates in a finite number of RNG
draws for any seed. Two constructs risk violating that guarantee if
users write their own:

- **Unbounded `loop { … }` retries.** A hand-written "keep drawing
  until the predicate holds" wedge on choice sequences where every
  draw fails. Use
  [`sample_with_rejection`]crate::sample_with_rejection instead —
  it enforces a `max_attempts` bound and, on exhaustion, rejects the
  enclosing case rather than looping forever.
- **Unbounded recursion.** A generator that calls itself without a
  decreasing depth argument (or an equivalent guard) can run without
  bound. Take an explicit depth (or count) parameter, decrease it in
  the recursive call, and return a base-case value when it hits zero.

Internally, noprop's shared bounded-domain sampler — the crate-private
core every finite-domain selection primitive draws through — is bounded
at 64 attempts per call. The per-attempt rejection rate is at worst ~50%
(when the requested domain is just above a power of two), so the
probability of exhausting all 64 attempts is `< 2⁻⁶⁴` — astronomically
unreachable in practice. If it does trigger inside `Runner::run`, the
current case is rejected (via
[`TestCaseContext::reject_case`](crate::TestCaseContext::reject_case));
outside a runner the call panics with a Runner-only message.

## Rejection scope

Rejection discards a value. noprop has two rejection scopes, and the
choice between them is a design decision the caller has to make:

- **Single-draw rejection** with
  [`sample_with_rejection`]crate::sample_with_rejection. Retries a
  local constrained draw (e.g. "an identifier: ASCII alphabetic or
  underscore, not starting with a digit") up to `max_attempts` times;
  only that draw is redrawn, not the surrounding case. This is the
  right scope when the constraint is a property of the value being
  drawn.
- **Whole-case rejection** with
  [`TestCaseContext::reject_case`]crate::TestCaseContext::reject_case.
  Discards the entire case as unsuitable after sampling has already
  finished (e.g. "this generated config line contains a comment, so it
  is not a record"). Rejected cases are retried and do not count toward
  the `cases` budget. This is the right scope when the precondition
  spans the whole case.

Total rejections across a run are bounded by an internal, `cases`-scaled
cap; a generator that always rejects still terminates with a
`TooManyRejections` failure. The cap is not a public knob; it is
generous relative to any realistic accept rate.

## Valid-by-construction

The best rejection scope is often *no rejection at all*. A
valid-by-construction generator picks a value that already satisfies
the constraint, instead of sampling widely and discarding failures.

Two typical shapes:

- **Sample the constraining parameter first, then satisfy it.** Draw
  the length with `sample_usize_in(ctx, 0..=max)`, then call
  `sample_bytes_vec(ctx, len)` or `sample_string(ctx, len)`. The
  result always has a length in range; there is no length rejection.
- **Compose within the domain.** For a `NonZeroU32`, either use
  bounded rejection over `sample_u32` (uniform, may reject) or map the
  underlying integer's zero to `1` explicitly (biased, always
  terminates in one draw). The two recipes are spelled out in the
  "Sampling non-zero integers" section of
  [`crate::docs::generator_authoring`] — pick the one whose trade-off
  matches the property.

When the constraint is expensive to satisfy at construction time —
e.g. "any two lists that share at least one element" — rejection is
often the pragmatic choice. Note when the accept rate is low
(≤ ~10%) so the caller can enlarge `max_attempts` accordingly.

## Recording the drawn value

The primitives (`sample_u32`, `sample_string`, …) record their drawn
value into the failure trace on every call. `record_generated` is
crate-private — user-defined wrappers cannot add their own trace
entry, so what the trace shows is the primitives' raw draws along
the wrapper's call path.

Marking a wrapper `#[track_caller]` attributes those primitive
entries to the user's call site (where the wrapper was invoked)
rather than to a line inside the wrapper — so a failure trace points
at where the caller asked for the value:

```rust
use std::num::NonZeroU32;
use noprop::TestCaseContext;

#[track_caller]
fn sample_non_zero_u32(ctx: &mut TestCaseContext) -> NonZeroU32 {
    let v = noprop::sample_u32(ctx);
    NonZeroU32::new(if v == 0 { 1 } else { v })
        .expect("v was remapped away from zero")
}
```

The trace here shows the raw `u32` the primitive returned, not the
`NonZeroU32` the wrapper produced; a reader reconstructs the
returned value from the wrapper's mapping. See the "Sampling
non-zero integers" section of [`crate::docs::generator_authoring`]
for the two `NonZero<_>` recipes and their trace trade-offs.