Skip to content

Dummies: AnyDecimal never generates the upper half of a range #206

Description

@Reefact

Problem

AnyDecimal draws a fraction that is supposed to be uniform in [0, 1), then maps it symmetrically around the midpoint of the requested range. The fraction is built from three Random.Next() calls (DecimalIntervalSpec.cs:145):

// A uniform-enough fraction in [0, 1): 93 random bits over the full decimal mantissa scale.
decimal fraction = new decimal(random.Next(), random.Next(), random.Next(), false, 28) / MaxFraction;
decimal mid       = _min / 2 + _max / 2;
decimal half      = _max / 2 - _min / 2;
decimal candidate = Clamped(mid + (fraction * 2 - 1) * half);

Random.Next() returns a non-negative int, so the top bit of each of the three 32-bit limbs of the 96-bit mantissa is always 0, and no limb can ever reach 2^31 - 1. But MaxFraction is the full 96-bit mantissa maximum (7.9228…, DecimalIntervalSpec.cs:14). The fraction therefore lives in [0, ~0.49999986], not [0, 1); (fraction * 2 - 1) lives in [-1, ~0); and every candidate lands in [min, mid).

Impact

The inclusive maximum documented on AnyDecimal.Between (AnyDecimal.cs:112) — and the entire upper half of every range — is unreachable. A test using Any.Decimal().Between(0m, 100m) to exercise "any valid percentage" silently never exercises 50–100. This inverts the library's core promise ("arbitrary yet valid, so hidden assumptions surface") into a hidden assumption of its own.

Reproduced against the built library: over 200,000 draws of Any.Decimal().Between(0m, 100m), the maximum observed was 49.99992….

Direction

Build the fraction from 96 genuinely uniform mantissa bits — any construction that fills all three limbs without fixing the top bit. For example:

byte[] limbs = new byte[12];
random.NextBytes(limbs);
decimal fraction = new decimal(
    BitConverter.ToInt32(limbs, 0),
    BitConverter.ToInt32(limbs, 4),
    BitConverter.ToInt32(limbs, 8),
    false, 28) / MaxFraction;

Any equivalent approach is acceptable provided the fraction covers [0, 1) uniformly and seeded reproducibility is preserved. Note the existing comment ("93 random bits over the full mantissa scale") already documents an intent the code does not meet.

Acceptance criteria

  • A seeded reachability test proves Any.Decimal().Between(lo, hi) observes values in both halves of the range, including near the inclusive maximum.
  • The fix preserves seeded reproducibility (same seed ⇒ same sequence within a version).
  • Point exclusions and allow-lists (Except, OneOf, DifferentFrom) still behave.
  • The stale [0, 1) comment is corrected to describe the real construction.

Context

Surfaced by the 2026-07-20 architecture & design audit of the standalone Dummies library (doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md, §4.1(a); branch claude/dummies-architecture-audit-cru5aq). The defect survived the green test suite because tests assert membership, never reachability — see the companion reachability-tests issue.


Filed from the Claude Code session that produced the 2026-07-20 Dummies architecture & design audit: https://claude.ai/code/session_01GvrWFVoG74xiys3oyEft1G

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions