mod-rand 1.0.0

Tiered randomness for Rust: fast PRNG, process-unique seeds, and OS-backed cryptographic random — plus bounded ranges, strings, tokens, shuffle, sample, and weighted choice. Zero dependencies, MSRV 1.75.
Documentation
# mod-rand — Project Specification (REPS)

> Rust Engineering Project Specification.
> Normative language follows RFC 2119.

## 1. Purpose

`mod-rand` MUST provide random number generation at three quality
tiers in a single zero-dependency library. Users pick the tier
appropriate to their threat model.

## 2. The three tiers

### Tier 1: Fast deterministic PRNG

MUST be:
- Seedable from a single `u64`
- Reproducible (same seed produces same stream)
- Fast (target: ~1 ns per `u64` on commodity hardware)
- Available in `no_std`
- Algorithm: xoshiro256\*\* with splitmix64 seeding

MUST NOT be:
- Used for security-sensitive randomness
- Marketed as cryptographically secure

### Tier 2: Process-unique seeds

MUST be:
- Different across calls within a process (counter-guaranteed)
- Different across processes with extremely high probability
- Fast (target: <100 ns per call)
- Available when `std` feature is enabled
- Algorithm: PID + nanosecond timestamp + atomic counter + per-process
  salt, mixed with a strong 64-bit avalanche function (Stafford
  variant 13)

MUST NOT be:
- Used for security-sensitive randomness
- Used where two simultaneous calls returning the same value would
  cause correctness bugs (the counter prevents this, but only within
  a single process)

### Tier 3: OS-backed cryptographic random

MUST be:
- Backed by the OS's secure random source:
  - Linux: `getrandom(2)` syscall
  - macOS: `getentropy(3)`
  - Windows: `BCryptGenRandom`
  - Other Unix: `/dev/urandom`
- Cryptographically secure (output unpredictable by an attacker)
- Available when `std` feature is enabled
- Resilient to fork / snapshot / VM-resume (no internal state to mix)

MUST NOT be:
- Optimized for speed at the cost of security
- Allowed to silently fall back to a weaker source on syscall failure
  (MUST return an error instead)

## 3. API surface

### 3.1 Tier 1

```rust
pub struct Xoshiro256 { /* private */ }

impl Xoshiro256 {
    // Construction
    pub fn seed_from_u64(seed: u64) -> Self;
    pub fn from_state(state: [u64; 4]) -> Option<Self>;
    pub fn state(&self) -> [u64; 4];

    // Raw draws
    pub fn next_u64(&mut self) -> u64;
    pub fn next_u32(&mut self) -> u32;
    pub fn next_f64(&mut self) -> f64;
    pub fn fill_bytes(&mut self, buf: &mut [u8]);

    // Bounded integer draws — half-open [start, end)
    pub fn gen_range_u64(&mut self, range: Range<u64>) -> u64;
    pub fn gen_range_u32(&mut self, range: Range<u32>) -> u32;
    pub fn gen_range_i64(&mut self, range: Range<i64>) -> i64;
    pub fn gen_range_i32(&mut self, range: Range<i32>) -> i32;

    // Bounded integer draws — inclusive [start, end]
    pub fn gen_range_inclusive_u64(&mut self, range: RangeInclusive<u64>) -> u64;
    pub fn gen_range_inclusive_u32(&mut self, range: RangeInclusive<u32>) -> u32;
    pub fn gen_range_inclusive_i64(&mut self, range: RangeInclusive<i64>) -> i64;
    pub fn gen_range_inclusive_i32(&mut self, range: RangeInclusive<i32>) -> i32;

    // Bounded float draw — half-open [start, end)
    pub fn gen_range_f64(&mut self, range: Range<f64>) -> f64;

    // Stream-splitting
    pub fn jump(&mut self);
    pub fn long_jump(&mut self);
}
```

### 3.2 Tier 2

```rust
// Unique values
pub fn unique_u64() -> u64;
pub fn unique_name(len: usize)   -> String;
pub fn unique_base32(len: usize) -> String;
pub fn unique_hex(len: usize)    -> String;

// Bounded integer draws — half-open
pub fn range_u64(range: Range<u64>) -> u64;
pub fn range_u32(range: Range<u32>) -> u32;
pub fn range_i64(range: Range<i64>) -> i64;
pub fn range_i32(range: Range<i32>) -> i32;

// Bounded integer draws — inclusive
pub fn range_inclusive_u64(range: RangeInclusive<u64>) -> u64;
pub fn range_inclusive_u32(range: RangeInclusive<u32>) -> u32;
pub fn range_inclusive_i64(range: RangeInclusive<i64>) -> i64;
pub fn range_inclusive_i32(range: RangeInclusive<i32>) -> i32;
```

### 3.3 Tier 3

```rust
// Raw cryptographic draws
pub fn fill_bytes(buf: &mut [u8]) -> io::Result<()>;
pub fn random_u32()               -> io::Result<u32>;
pub fn random_u64()               -> io::Result<u64>;
pub fn random_bytes(len: usize)   -> io::Result<Vec<u8>>;
pub fn random_hex(bytes: usize)   -> io::Result<String>;
pub fn random_base32(chars: usize) -> io::Result<String>;

// Bounded integer draws — half-open
pub fn random_range_u64(range: Range<u64>) -> io::Result<u64>;
pub fn random_range_u32(range: Range<u32>) -> io::Result<u32>;
pub fn random_range_i64(range: Range<i64>) -> io::Result<i64>;
pub fn random_range_i32(range: Range<i32>) -> io::Result<i32>;

// Bounded integer draws — inclusive
pub fn random_range_inclusive_u64(range: RangeInclusive<u64>) -> io::Result<u64>;
pub fn random_range_inclusive_u32(range: RangeInclusive<u32>) -> io::Result<u32>;
pub fn random_range_inclusive_i64(range: RangeInclusive<i64>) -> io::Result<i64>;
pub fn random_range_inclusive_i32(range: RangeInclusive<i32>) -> io::Result<i32>;
```

## 4. Bounded-range semantics

The bounded-range API MUST satisfy these properties on every tier:

### 4.1 Range syntax

- `start..end` (`Range<T>`) MUST be treated as half-open: the produced
  value satisfies `start <= v < end`.
- `start..=end` (`RangeInclusive<T>`) MUST be treated as inclusive:
  the produced value satisfies `start <= v <= end`.

The caller's choice of `..` vs `..=` IS the contract. No tier accepts
a two-argument `(min, max)` form; the ambiguity that would create is
the reason we use range syntax.

### 4.2 Uniformity (no modulo bias)

For any non-empty integer range, every value in the range MUST be
producible, and over a large number of draws the empirical
distribution MUST converge to uniform. Implementations MUST NOT use
naive `value % n` reduction; they MUST use unbiased rejection
sampling.

The reference algorithm is Daniel Lemire's "Nearly Divisionless"
random integer generation (J. ACM 2019). Equivalent algorithms with
the same uniformity guarantee are acceptable.

The crate's integration tests MUST include at least one chi-squared
uniformity test over a large sample (≥1,000,000 draws) for at least
one tier; this test MUST pass before any release.

### 4.3 Invalid ranges

- An empty range (`start >= end` for half-open; `start > end` for
  inclusive) is a programming error.
  - Tier 1 and Tier 2 MUST panic with a descriptive message.
  - Tier 3 MUST return `io::Error` with `ErrorKind::InvalidInput` and
    a descriptive message.
- A single-value inclusive range (`start..=start`) MUST return
  `start` without a redundant draw.
- The full-width inclusive range (`T::MIN..=T::MAX`) MUST be supported
  and equivalent to a raw `next_uN` / `random_uN` draw.

### 4.4 Float ranges

- Only `Range<f64>` (half-open) is supported. There is no
  `RangeInclusive<f64>` API; the probability of producing either
  endpoint exactly is zero in any case.
- If either bound is non-finite (NaN or infinity), Tier 1 MUST panic.
- If `start >= end`, Tier 1 MUST panic.
- The implementation uses `next_f64()` (a uniform draw in `[0, 1)`)
  and maps it linearly into the requested range.

### 4.5 Signed integers

- For `Range<i32>` / `Range<i64>` and the corresponding
  `RangeInclusive`, the span between bounds is computed in a wider
  unsigned type (u128 internally) to avoid signed overflow at extreme
  endpoints.
- `RangeInclusive<i64>` covering the entire `i64::MIN..=i64::MAX`
  range MUST be supported and equivalent to reinterpreting a raw
  `next_u64` / `random_u64` draw as `i64`.

## 5. Determinism

- Tier 1 output MUST be deterministic given a seed. This includes
  bounded-range output: replaying a seed produces the same sequence
  of bounded values from the same sequence of `gen_range_*` calls.
- Tier 2 output MUST be unique across calls within a process.
  Bounded-range Tier 2 output makes no uniqueness guarantee (the
  reduction maps multiple `u64` inputs to the same bounded value by
  construction).
- Tier 3 output MUST be non-deterministic.

## 6. Dependencies

This crate MUST NOT have runtime dependencies outside of `std` (when
the `std` feature is enabled). Platform-specific FFI declarations
SHOULD be inlined rather than pulled in via `libc`.

The point: this crate exists partly to break the `getrandom`-induced
MSRV ratchet. We cannot replace one MSRV-locking dep with another.

## 7. Stability

- Through `0.9.x` the public API MAY shift in minor ways.
- `1.0.0` pins the API, the tier definitions, and the Tier 1
  reproducibility contract for the entire `1.x` line. See
  [`docs/STABILITY.md`]docs/STABILITY.md for the binding SemVer
  policy.

## 8. Scope at 1.0

Operations covered (locked under the `1.x` SemVer contract):

- All three tiers (Tier 1 PRNG, Tier 2 process-unique, Tier 3 OS
  cryptographic), each with raw draws, byte fills, and bounded
  integer ranges on every primitive integer width (`u8` / `u16` /
  `u32` / `u64` / `u128` / `usize` and signed counterparts), both
  half-open (`..`) and inclusive (`..=`).
- Tier 1 distribution helpers: `gen_f64`, `gen_f32`, `gen_bool`.
- String generation on every tier: `random_string` / `gen_string`
  taking a `&[u8]` charset, plus convenience methods for the most
  common cases (`*_alphanumeric`, `*_alpha`, `*_numeric`, `*_hex`).
  Charset constants live in `mod_rand::charsets`.
- Collection operations on Tier 1: `shuffle`, `sample`,
  `weighted_index`, `weighted_choice`. Cryptographic shuffle on
  Tier 3.

## 9. Out of scope (1.x)

Adding any of these in a later minor release is fine as a pure
addition; the list below is what 1.0 explicitly does NOT ship:

- Named distributions (Normal, Poisson, Exponential, Beta, Gamma,
  etc.). Defer to `rand_distr` if you need them. The raw uniform +
  Bernoulli helpers in this crate are the building blocks; named
  distributions are 1.1+ work if demand surfaces.
- Trait-heavy abstraction (`Rng` / `RngCore` / `SeedableRng`) — this
  crate exposes concrete types only.
- Hardware random sources (RDRAND, RDSEED) — may revisit if there is
  demand.
- Multi-thread coordination (shared atomic-state RNGs) — Tier 2's
  atomic counter is enough; if users want a shared Tier 1, they wrap
  one themselves.
- Async APIs. mod-rand stays sync.
- Serde integration for `Xoshiro256` state. Candidate for 1.x minor.
- Tier 2 / Tier 3 collection ops beyond Tier 3 shuffle. Tier 1
  covers the use cases; if demand surfaces for cryptographic sample
  / weighted choice, those land as 1.x minor additions.
- An alias-method weighted-choice variant. Candidate for 1.x minor
  if demand surfaces for high-throughput pre-built weight tables.