From 99caef393ad8c7fc5d2c91f14b49dd00fb7a2e18 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:17:24 +0000 Subject: [PATCH 1/7] fix(dummies): let out-of-domain contained values extend cardinality A distinct collection (SetOf, ListOf(...).Distinct(), dictionary keys) gated its element count against the element generator's cardinality alone, so it rejected satisfiable requests whose Containing(...) values lie outside that domain -- e.g. Any.SetOf(Any.Int32().OneOf(1, 2)) .Containing(3).WithCount(3), where {1, 2, 3} is reachable. Model the effective distinct domain instead. A new internal IDomainMembership lets a generator answer whether a value is one it could produce; every finite-cardinality generator now implements it. CollectionState counts only the elements that must come from the generator: it subtracts the contained values proven outside the domain (and each opaque ContainingAny draw) from the required count, and raises the resolution cap by the same out-of-domain values. A value already inside the domain still does not inflate capacity, so genuinely impossible requests keep failing eagerly; an unprovable overlap defers to the bounded generation-time draw rather than a false conflict. The check is written in subtractive form so it cannot overflow for a near-long.MaxValue element domain, and membership is decided under the generator's own equality so it stays a sound upper bound under a custom comparer. Refine the still-Proposed ADR-0013 (its context and the rationale claim that the eager check "never rejects a satisfiable request", which this case disproved) and the user-facing wording, and add tests across SetOf, ListOf().Distinct(), custom comparers and order-independent declaration. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- Dummies.UnitTests/AnyCollectionTests.cs | 100 ++++++++++++++++++ Dummies/AnyBool.cs | 5 +- Dummies/AnyByte.cs | 4 +- Dummies/AnyChar.cs | 5 +- Dummies/AnyDateOnly.cs | 4 +- Dummies/AnyDateTime.cs | 4 +- Dummies/AnyDateTimeOffset.cs | 4 +- Dummies/AnyEnum.cs | 5 +- Dummies/AnyGuid.cs | 10 +- Dummies/AnyInt16.cs | 4 +- Dummies/AnyInt32.cs | 4 +- Dummies/AnyInt64.cs | 4 +- Dummies/AnySByte.cs | 4 +- Dummies/AnyTimeOnly.cs | 4 +- Dummies/AnyTimeSpan.cs | 4 +- Dummies/AnyUInt16.cs | 4 +- Dummies/AnyUInt32.cs | 4 +- Dummies/AnyUInt64.cs | 4 +- Dummies/CollectionState.cs | 48 +++++++-- Dummies/ICardinalityHint.cs | 5 + Dummies/IDomainMembership.cs | 37 +++++++ Dummies/OrdinalIntervalSpec.cs | 12 +++ Dummies/README.nuget.md | 3 +- ...ons-by-cardinality-else-bounded-draw.fr.md | 31 ++++-- ...ctions-by-cardinality-else-bounded-draw.md | 27 +++-- 25 files changed, 295 insertions(+), 45 deletions(-) create mode 100644 Dummies/IDomainMembership.cs diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index f776fd74..24de9772 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -147,6 +147,106 @@ public void ContainingFromAGenerator() { } } + [Fact(DisplayName = "Containing: a fixed value outside the element domain extends the effective cardinality (issue #188).")] + public void ContainingOutsideDomainExtendsCardinality() { + // The motivating case: {1, 2, 3} is satisfiable — 3 is supplied directly and lies outside the {1, 2} the + // generator can produce, so only two elements must be drawn from it. + for (int i = 0; i < SampleCount; i++) { + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3); + Check.That(set).Contains(1, 2, 3); + Check.That(set.Count).IsEqualTo(3); + } + + // The same reasoning holds for a distinct list, for dictionary-shaped keys, and for several out-of-domain + // values at once — the correction applies wherever a distinct collection is gated by cardinality. + for (int i = 0; i < SampleCount; i++) { + HashSet list = new(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate()); + Check.That(list).Contains(1, 2, 3); + + HashSet quad = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).Containing(4).WithCount(4); + Check.That(quad).Contains(1, 2, 3, 4); + Check.That(quad.Count).IsEqualTo(4); + } + } + + [Fact(DisplayName = "Containing: a value already inside the element domain does not inflate the cardinality, so an impossible count still conflicts.")] + public void ContainingInsideDomainDoesNotInflate() { + // 1 is already producible by the generator, so it adds no capacity: three distinct values over {1, 2} remain + // impossible and must still fail eagerly, naming the shortfall. + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).WithCount(3)); + Check.That(conflict.Message).Contains("2 distinct value"); + + // Mixed: 1 is inside the domain, 5 is outside — effective capacity is 2 + 1 = 3. Four is over the top; three + // is exactly reachable as {1, 2, 5}. + Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(4)).Throws(); + for (int i = 0; i < SampleCount; i++) { + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(3); + Check.That(set).Contains(1, 2, 5); + } + } + + [Fact(DisplayName = "Containing: the effective cardinality is order-independent across Distinct, Containing and the count.")] + public void EffectiveCardinalityIsOrderIndependent() { + // Every ordering of the same three constraints reaches the same verdict — accepted, because 3 is outside the + // domain — since Distinct() re-runs the whole validation on the accumulated state. + for (int i = 0; i < SampleCount; i++) { + Check.That(new HashSet(Any.ListOf(Any.Int32().OneOf(1, 2)).WithCount(3).Containing(3).Distinct().Generate())).Contains(1, 2, 3); + Check.That(new HashSet(Any.ListOf(Any.Int32().OneOf(1, 2)).Distinct().Containing(3).WithCount(3).Generate())).Contains(1, 2, 3); + Check.That(new HashSet(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate())).Contains(1, 2, 3); + } + + // And rejected whatever the order, because the contained value is inside the domain. + Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).WithCount(3).Containing(1).Distinct()).Throws(); + Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).Distinct().Containing(1).WithCount(3)).Throws(); + Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(1).WithCount(3).Distinct()).Throws(); + } + + [Fact(DisplayName = "Containing: a near-maximum element cardinality plus an out-of-domain value does not overflow into a false conflict.")] + public void ContainingNearMaximumCardinalityDoesNotOverflow() { + // Between(0, long.MaxValue - 1) advertises long.MaxValue distinct values; the additive form base + extras + // would overflow to a negative and reject spuriously. The subtractive check stays correct. + for (int i = 0; i < SampleCount; i++) { + HashSet set = Any.SetOf(Any.Int64().Between(0, long.MaxValue - 1)).Containing(-1L).WithCount(3); + Check.That(set).Contains(-1L); + Check.That(set.Count).IsEqualTo(3); + } + } + + [Fact(DisplayName = "ContainingAny stays conservative: no eager false conflict, and an opaque shortfall surfaces at generation.")] + public void ContainingAnyDefersToGeneration() { + // The generator drawn from can yield a value outside the element domain, so the request cannot be proven + // impossible at declaration — a wide ContainingAny makes it genuinely satisfiable. + for (int i = 0; i < SampleCount; i++) { + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).ContainingAny(Any.Int32().GreaterThan(100)).WithCount(3); + Check.That(set.Count).IsEqualTo(3); + Check.That(set).Contains(1, 2); + } + + // When every source draws from the same two-value domain, three distinct values are impossible — but the + // overlap is opaque, so it is caught while drawing (a replayable AnyGenerationException) rather than as a + // false eager conflict. + Check.ThatCode(() => Any.SetOf(Any.Bool()).ContainingAny(Any.Bool()).ContainingAny(Any.Bool()).ContainingAny(Any.Bool()).Generate()) + .Throws(); + } + + [Fact(DisplayName = "Containing under a merging comparer: an out-of-domain value is credited, and a comparer that merges it back is caught at generation.")] + public void ContainingUnderAMergingComparer() { + IEqualityComparer modTen = new ModuloComparer(10); + + // 15 is outside {1, 2, 3} and its residue class (5) is fresh too, so {1, 2, 3, 15} has four classes and + // generation succeeds. + for (int i = 0; i < SampleCount; i++) { + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2, 3), modTen).Containing(15).WithCount(4); + Check.That(set.Count).IsEqualTo(4); + } + + // 12 is outside {1, 2} by value, so it is still credited and the request is accepted eagerly — but 12 ≡ 2 + // (mod 10) collapses it back into the domain, so three distinct-under-the-comparer values are impossible and + // the shortfall surfaces while drawing, never as a false eager conflict. + Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2), modTen).Containing(12).WithCount(3).Generate()).Throws(); + } + [Fact(DisplayName = "ArrayOf: produces an array of the requested size, distinct when asked.")] public void ArrayOfProducesArrays() { for (int i = 0; i < SampleCount; i++) { diff --git a/Dummies/AnyBool.cs b/Dummies/AnyBool.cs index f1c3913c..e8acea5e 100644 --- a/Dummies/AnyBool.cs +++ b/Dummies/AnyBool.cs @@ -5,7 +5,7 @@ namespace Dummies; /// mostly useful for symmetry when a test sweeps cases — and contradictory pins fail eagerly with a /// naming both sides, like every other generator. /// -public sealed class AnyBool : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyBool : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -38,6 +38,9 @@ private AnyBool(RandomSource source, bool? pinned, string? pinnedConstraint) { // Two distinct values unless a pin has already fixed one of them. long? ICardinalityHint.DistinctCardinality => _pinned is null ? 2 : 1; + // A pin narrows the domain to that single value; unpinned, both booleans are producible. + bool IDomainMembership.Contains(bool value) => _pinned is not bool pinned || pinned == value; + /// Pins the value to true. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyByte.cs b/Dummies/AnyByte.cs index 5f75b1f2..fc9bf97d 100644 --- a/Dummies/AnyByte.cs +++ b/Dummies/AnyByte.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnyByte(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(byte value) => _spec.Contains(Ord(value)); + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyChar.cs b/Dummies/AnyChar.cs index 56ce746b..a4ce3abd 100644 --- a/Dummies/AnyChar.cs +++ b/Dummies/AnyChar.cs @@ -8,7 +8,7 @@ namespace Dummies; /// . A combination that empties the pool fails eagerly with a /// . /// -public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -65,6 +65,9 @@ private AnyChar(RandomSource source, // The pool is materialized once at construction, so its size is the exact number of characters drawable. long? ICardinalityHint.DistinctCardinality => _pool.Count; + // The pool is the exact draw set, so membership is a direct pool lookup. + bool IDomainMembership.Contains(char value) => _pool.Contains(value); + /// Restricts the character to ASCII letters only. Declared once per generator. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyDateOnly.cs b/Dummies/AnyDateOnly.cs index a8c8f449..0ca3839d 100644 --- a/Dummies/AnyDateOnly.cs +++ b/Dummies/AnyDateOnly.cs @@ -16,7 +16,7 @@ namespace Dummies; /// constraint: a reproducible test pins its reference dates explicitly with and /// . /// -public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -58,6 +58,8 @@ private AnyDateOnly(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(DateOnly value) => _spec.Contains(Ord(value)); + /// Requires a date strictly after . /// The exclusive lower bound. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyDateTime.cs b/Dummies/AnyDateTime.cs index 80402520..05a00658 100644 --- a/Dummies/AnyDateTime.cs +++ b/Dummies/AnyDateTime.cs @@ -19,7 +19,7 @@ namespace Dummies; /// comparison operators do. Values supplied to are returned as given, Kind included. There is deliberately no clock-relative constraint (no "in the past/future"): a /// reproducible test pins its reference instants explicitly with and . /// -public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -63,6 +63,8 @@ private AnyDateTime(RandomSource source, OrdinalIntervalSpec spec, IReadOnlyDict long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(DateTime value) => _spec.Contains(Ord(value)); + /// Requires an instant strictly after . /// The exclusive lower bound. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyDateTimeOffset.cs b/Dummies/AnyDateTimeOffset.cs index 0859094d..7051c8bf 100644 --- a/Dummies/AnyDateTimeOffset.cs +++ b/Dummies/AnyDateTimeOffset.cs @@ -21,7 +21,7 @@ namespace Dummies; /// past/future"): a reproducible test pins its reference instants explicitly with and /// . /// -public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -65,6 +65,8 @@ private AnyDateTimeOffset(RandomSource source, OrdinalIntervalSpec spec, IReadOn long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(DateTimeOffset value) => _spec.Contains(Ord(value)); + /// Requires an instant strictly after . /// The exclusive lower bound. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyEnum.cs b/Dummies/AnyEnum.cs index 4f0d0df1..abde3bae 100644 --- a/Dummies/AnyEnum.cs +++ b/Dummies/AnyEnum.cs @@ -7,7 +7,7 @@ namespace Dummies; /// fails eagerly with a naming both sides. /// /// The enum type to draw values from. -public sealed class AnyEnum : IAny, IHasRandomSource, ICardinalityHint +public sealed class AnyEnum : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership where TEnum : struct, Enum { #region Statics members declarations @@ -61,6 +61,9 @@ private AnyEnum(RandomSource source, IReadOnlyList declared, // The pool is materialized once at construction, so its size is the exact number of members drawable. long? ICardinalityHint.DistinctCardinality => _pool.Count; + // The pool is the exact draw set, so membership is a direct pool lookup. + bool IDomainMembership.Contains(TEnum value) => _pool.Contains(value); + /// Requires the value to be one of the supplied members. Declared once per generator. /// The allowed members; duplicates are ignored. Every value must be a declared member of — the generator never yields undeclared numeric values, not even explicitly supplied ones. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyGuid.cs b/Dummies/AnyGuid.cs index 97c2a8e0..5a67f79c 100644 --- a/Dummies/AnyGuid.cs +++ b/Dummies/AnyGuid.cs @@ -8,7 +8,7 @@ namespace Dummies; /// to pin the empty identifier. Contradictory constraints fail eagerly with a /// naming both sides. /// -public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -68,6 +68,14 @@ private AnyGuid(RandomSource source, Guid? pinned, string? pinnedConstraint, // Pinned to a single value, or bounded by an allow-list; otherwise the domain is effectively unbounded. long? ICardinalityHint.DistinctCardinality => _pinned is not null ? 1 : _effectiveAllowed?.Count; + // Mirrors Generate: the pin, then the allow-list, then the full space minus the exclusions. + bool IDomainMembership.Contains(Guid value) { + if (_pinned is Guid pinned) { return pinned == value; } + if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } + + return !_excluded.Contains(value); + } + /// Requires an identifier different from . /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyInt16.cs b/Dummies/AnyInt16.cs index 1cc212ee..a31187c2 100644 --- a/Dummies/AnyInt16.cs +++ b/Dummies/AnyInt16.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnyInt16(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(short value) => _spec.Contains(Ord(value)); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyInt32.cs b/Dummies/AnyInt32.cs index c8169f90..377b4a0a 100644 --- a/Dummies/AnyInt32.cs +++ b/Dummies/AnyInt32.cs @@ -28,7 +28,7 @@ namespace Dummies; /// /// /// -public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -70,6 +70,8 @@ private AnyInt32(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(int value) => _spec.Contains(Ord(value)); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyInt64.cs b/Dummies/AnyInt64.cs index a64bd925..a4ef0337 100644 --- a/Dummies/AnyInt64.cs +++ b/Dummies/AnyInt64.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnyInt64(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(long value) => _spec.Contains(Ord(value)); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnySByte.cs b/Dummies/AnySByte.cs index 9647ad62..6a0fd6ee 100644 --- a/Dummies/AnySByte.cs +++ b/Dummies/AnySByte.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnySByte(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(sbyte value) => _spec.Contains(Ord(value)); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyTimeOnly.cs b/Dummies/AnyTimeOnly.cs index d72960f4..f7c0245b 100644 --- a/Dummies/AnyTimeOnly.cs +++ b/Dummies/AnyTimeOnly.cs @@ -16,7 +16,7 @@ namespace Dummies; /// constraint: a reproducible test pins its reference time of days explicitly with and /// . /// -public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -58,6 +58,8 @@ private AnyTimeOnly(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(TimeOnly value) => _spec.Contains(Ord(value)); + /// Requires a time of day strictly after . /// The exclusive lower bound. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyTimeSpan.cs b/Dummies/AnyTimeSpan.cs index b6edb664..c6626258 100644 --- a/Dummies/AnyTimeSpan.cs +++ b/Dummies/AnyTimeSpan.cs @@ -13,7 +13,7 @@ namespace Dummies; /// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one /// draw. Unconstrained, it draws from the full range, negative durations included. /// -public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -55,6 +55,8 @@ private AnyTimeSpan(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(TimeSpan value) => _spec.Contains(Ord(value)); + /// Requires a duration strictly greater than . /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyUInt16.cs b/Dummies/AnyUInt16.cs index 2586729b..4b2602f6 100644 --- a/Dummies/AnyUInt16.cs +++ b/Dummies/AnyUInt16.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnyUInt16(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(ushort value) => _spec.Contains(Ord(value)); + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyUInt32.cs b/Dummies/AnyUInt32.cs index c1268f7a..ef535419 100644 --- a/Dummies/AnyUInt32.cs +++ b/Dummies/AnyUInt32.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnyUInt32(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(uint value) => _spec.Contains(Ord(value)); + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyUInt64.cs b/Dummies/AnyUInt64.cs index fec29a3b..4efb26ba 100644 --- a/Dummies/AnyUInt64.cs +++ b/Dummies/AnyUInt64.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { #region Statics members declarations @@ -54,6 +54,8 @@ private AnyUInt64(RandomSource source, OrdinalIntervalSpec spec) { long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + bool IDomainMembership.Contains(ulong value) => _spec.Contains(Ord(value)); + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/CollectionState.cs b/Dummies/CollectionState.cs index 5586e3bc..d78bcaa8 100644 --- a/Dummies/CollectionState.cs +++ b/Dummies/CollectionState.cs @@ -16,10 +16,12 @@ namespace Dummies; /// /// /// Distinctness follows the two-layer contract the library commits to: when the element generator advertises a -/// small cardinality that a requested count would exceed, the conflict is -/// caught at declaration time (); otherwise the count is drawn and -/// the elements are filled by a bounded dedup-draw, and a genuine shortfall surfaces at generation as an -/// naming the seed to replay. +/// small cardinality that the requested count would exceed — counting only +/// the elements that must be drawn from that generator, since values pinned with Containing(...) outside +/// its domain (see ) are supplied directly and extend the effective domain — +/// the conflict is caught at declaration time (); otherwise the +/// count is drawn and the elements are filled by a bounded dedup-draw, and a genuine shortfall surfaces at +/// generation as an naming the seed to replay. /// /// The element type. internal sealed class CollectionState { @@ -166,15 +168,45 @@ private void Validate(string applying) { } if (_itemCardinality is long cardinality) { - int need = Math.Max(_count.Floor, required); - if (need > cardinality) { - throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {Elements(need)} required to be distinct exceed the {cardinality.ToString(CultureInfo.InvariantCulture)} distinct value(s) the element generator can produce."); + // Only the elements that must be drawn from the generator count against its cardinality: values pinned + // outside its domain occupy their own slots, and each opaque ContainingAny draw is credited as if it + // could land outside too (conservative — an unprovable overlap defers to the bounded draw rather than + // a false conflict). The subtractive form keeps the left side within int, so a near-long.MaxValue + // domain never overflows the comparison. + int need = Math.Max(_count.Floor, required); + int fromGenerator = need - FixedOutsideCount() - _generatedContaining.Count; + if (fromGenerator > cardinality) { + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {Elements(fromGenerator)} required to be distinct exceed the {cardinality.ToString(CultureInfo.InvariantCulture)} distinct value(s) the element generator can produce."); } } } private int? CardinalityCap() { - return _itemCardinality is long cardinality && cardinality <= int.MaxValue ? (int)cardinality : null; + // The effective ceiling is the generator's own cardinality plus the values pinned outside its domain, which + // fill their own slots without drawing on it — so a distinct collection over a small domain can still reach + // the size those extra values allow. Guard the cast: a domain wider than int cannot cap an int count anyway, + // and once the generator's cardinality is int-bounded the add stays within long. + if (_itemCardinality is not long cardinality || cardinality > int.MaxValue) { return null; } + + long effective = cardinality + FixedOutsideCount(); + + return effective <= int.MaxValue ? (int)effective : null; + } + + private int FixedOutsideCount() { + if (_fixedContaining.Count == 0) { return 0; } + // A fixed value the element generator could never produce extends the effective distinct domain; a value + // already inside it does not. When the generator cannot answer membership (it advertises a cardinality but + // not a domain), stay conservative and treat every fixed value as outside: the eager check then never + // rejects a request that might be satisfiable, and a genuine shortfall is caught by the bounded draw. + if (_item is not IDomainMembership membership) { return _fixedContaining.Count; } + + int outside = 0; + foreach (T value in _fixedContaining) { + if (!membership.Contains(value)) { outside++; } + } + + return outside; } private T DrawFresh(IAny generator, HashSet seen, RandomSource source, int target) { diff --git a/Dummies/ICardinalityHint.cs b/Dummies/ICardinalityHint.cs index 54b64ac4..e37d3a52 100644 --- a/Dummies/ICardinalityHint.cs +++ b/Dummies/ICardinalityHint.cs @@ -14,6 +14,11 @@ namespace Dummies; /// the bounded dedup-draw fallback, which surfaces a genuine shortfall as an . /// Because the bound ignores any custom (which can only merge values, /// never create new ones), it stays a sound upper bound under a comparer too. +/// +/// A generator that advertises a finite cardinality here must also implement +/// , so a distinct collection can tell whether a value pinned with +/// Containing(...) extends the domain or already sits inside it; the two capabilities travel together. +/// /// internal interface ICardinalityHint { diff --git a/Dummies/IDomainMembership.cs b/Dummies/IDomainMembership.cs new file mode 100644 index 00000000..a7e8c96c --- /dev/null +++ b/Dummies/IDomainMembership.cs @@ -0,0 +1,37 @@ +namespace Dummies; + +/// +/// Companion to : implemented by the library's own generators that draw from a +/// small, countable domain so a distinct collection can decide, at declaration time, whether a value +/// pinned with Containing(...) lies inside or outside the element generator's domain. A fixed value the +/// generator could never produce ( returns false) adds one to the effective +/// distinct cardinality — it is a value the generator itself cannot draw — while a value already in the domain +/// does not. See for how the effective cardinality gates satisfiability. +/// +/// +/// +/// The pairing is load-bearing: every generator that advertises a finite +/// must also implement this interface, and +/// must agree exactly with that finite domain — the same predicate the generator +/// honours when it draws. Cardinality answers "how many", membership answers "which". A generator whose +/// domain is unbounded or unknown implements neither; the collection then relies on the bounded dedup-draw +/// fallback. Should a finite-cardinality generator ever omit this interface, +/// stays safe by treating every contained value as outside the domain — conservative, so the eager check +/// never rejects a request that might be satisfiable. +/// +/// +/// Membership is decided under the generator's own (default) equality. A custom collection +/// can only merge values, never create new ones, so a value that +/// is "provably outside" under default equality remains a sound upper-bound contribution to the effective +/// cardinality; a comparer that collapses it back into the domain is caught by the bounded draw instead. +/// +/// +/// The element type. +internal interface IDomainMembership { + + /// Whether the generator, as constrained, could ever produce . + /// The candidate value. + /// true when is within the generator's domain; otherwise false. + bool Contains(T value); + +} diff --git a/Dummies/OrdinalIntervalSpec.cs b/Dummies/OrdinalIntervalSpec.cs index cc0246cd..93aa4a06 100644 --- a/Dummies/OrdinalIntervalSpec.cs +++ b/Dummies/OrdinalIntervalSpec.cs @@ -162,6 +162,18 @@ internal long? Cardinality { } } + /// + /// Whether is a value the specification could produce — the exact domain + /// draws from: a member of the allow-list when one is set, otherwise inside + /// the interval and not excluded. Feeds , so a distinct collection can tell + /// a contained value that extends the domain from one already inside it. + /// + internal bool Contains(ulong ordinal) { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(ordinal); } + + return ordinal >= _min && ordinal <= _max && !_excludedInRange.Contains(ordinal); + } + /// Draws one ordinal satisfying the whole specification — built directly, never generate-then-retry. internal ulong GenerateOrdinal(Random random) { if (_effectiveAllowed is not null) { diff --git a/Dummies/README.nuget.md b/Dummies/README.nuget.md index 368079cd..00901874 100644 --- a/Dummies/README.nuget.md +++ b/Dummies/README.nuget.md @@ -45,7 +45,8 @@ matter — and that is the point. - **Collections over any element generator**: `Any.ListOf(item)`, `ArrayOf`, `SequenceOf`, `SetOf` and `DictionaryOf`, constrained with `WithCount`/`NonEmpty`/`Distinct`/`Containing`. A distinct collection asked for more - elements than its element domain can produce fails fast, just like any other + distinct elements than its effective domain — the element generator plus any values + pinned outside it with `Containing` — can produce fails fast, just like any other conflict; `Any.PairOf`/`TripleOf` pair generators into value tuples. - **Optional values**: `.OrNull()` turns any generator into one that is `null` about half the time and otherwise a constrained value — the dummy for an optional field, diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md index ab00ea09..2f3c9b7f 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md @@ -16,9 +16,13 @@ jamais générée puis filtrée, et jamais derrière une boucle de réessai. L'incrément « collections » ajoute les collections distinctes : `SetOf`, `ListOf(...).Distinct()` et consorts, ainsi que les clés d'un dictionnaire. Une -collection distincte de *N* éléments n'est satisfaisable que si son générateur -d'éléments peut produire au moins *N* valeurs distinctes, ce qui est une propriété -du domaine de ce générateur. +collection distincte de *N* éléments n'est satisfaisable que si *N* valeurs +distinctes peuvent être assemblées depuis son **domaine effectif** : le domaine +propre du générateur d'éléments, élargi par les valeurs fixées avec `Containing(...)` +qui en sortent — chacune est une valeur que le générateur lui-même ne pourrait +jamais tirer — et par les tirages opaques de `ContainingAny(...)`. La cardinalité du +seul générateur d'éléments ne borne donc que les éléments qui doivent venir *de +lui*, non la demande entière. Les générateurs d'éléments se répartissent en deux groupes. Certains tirent d'un domaine petit et dénombrable : un booléen a deux valeurs, une énumération a ses @@ -57,11 +61,17 @@ petit. épuisement, il rapporte le manque réel via une `AnyGenerationException` nommant la graine — le canal d'échec qu'utilise déjà un rejet de fabrique — plutôt que de boucler. -* **La borne annoncée reste correcte sous un comparateur.** Puisqu'un comparateur - ne fait que fusionner des valeurs, la cardinalité annoncée reste une borne - *supérieure* valide : le contrôle anticipé ne rejette donc jamais une demande qui - était en réalité satisfaisable ; un comparateur qui réduit le domaine sous le - nombre demandé est rattrapé par le tirage borné. +* **Le contrôle anticipé ne compte que ce que le générateur doit fournir, et reste + correct sous un comparateur.** Il compare la cardinalité du générateur d'éléments + au nombre demandé *diminué* des valeurs fixées hors de son domaine et de chaque + tirage opaque de `ContainingAny(...)` — ainsi une valeur fixe que le générateur ne + peut produire élargit la demande au lieu d'entrer en conflit avec elle, et un + recouvrement improuvable est renvoyé au tirage borné plutôt que de devenir un faux + conflit. Puisqu'un comparateur ne fait que fusionner des valeurs, la cardinalité + annoncée reste une borne *supérieure* valide sur la contribution propre du + générateur : le contrôle ne rejette donc jamais une demande qui était en réalité + satisfaisable ; un comparateur qui réduit le domaine effectif sous le nombre + demandé est rattrapé par le tirage borné. * **Un seul principe, appliqué là où son information existe.** Répartir l'échec entre la déclaration (quand le domaine est dénombrable) et la génération (quand il ne l'est pas) n'est pas un affaiblissement du principe de conflit anticipé @@ -140,5 +150,6 @@ principe de travail borné de la bibliothèque. ## Références * ADR-0011 — Héberger Dummies comme un paquet autonome dans ce dépôt. -* Le moteur de collection distincte et la capacité de cardinalité, dans le projet - `Dummies` (`CollectionState`, `ICardinalityHint`). +* Le moteur de collection distincte et les capacités de cardinalité et + d'appartenance, dans le projet `Dummies` (`CollectionState`, `ICardinalityHint`, + `IDomainMembership`). diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md index 6f3b88e5..6b4467ab 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md @@ -16,8 +16,12 @@ never behind a retry loop. The collection increment adds distinct collections: `SetOf`, `ListOf(...).Distinct()` and the like, and a dictionary's keys. A distinct collection of *N* elements is -satisfiable only if its element generator can yield at least *N* distinct values, -which is a property of that generator's domain. +satisfiable only if *N* distinct values can be assembled from its **effective +domain**: the element generator's own domain, widened by any values pinned with +`Containing(...)` that fall outside it — each such value is one the generator itself +could never draw — and by the opaque draws of `ContainingAny(...)`. The element +generator's cardinality alone therefore bounds only the elements that must come +*from* it, not the whole request. Element generators fall into two groups. Some draw from a small, countable domain: a boolean has two values, an enum has its declared members, a narrow @@ -51,11 +55,16 @@ replayable seed, if the element domain proves too small. bounded honours the no-retry-loop principle; on exhaustion it reports the real shortfall as an `AnyGenerationException` naming the seed — the same failure channel a factory rejection already uses — rather than looping. -* **The advertised bound stays sound under a comparer.** Since a comparer only - merges values, the advertised cardinality remains a valid *upper* bound, so the - eager check never rejects a request that was actually satisfiable; a comparer - that collapses the domain below the requested count is caught by the bounded - draw instead. +* **The eager check counts only what the generator must supply, and stays sound + under a comparer.** It compares the element generator's cardinality against the + count *reduced by* the values pinned outside its domain and by each opaque + `ContainingAny(...)` draw — so a fixed value the generator cannot produce widens + the request instead of conflicting with it, and an unprovable overlap defers to + the bounded draw rather than becoming a false conflict. Because a comparer only + merges values, the advertised cardinality remains a valid *upper* bound on the + generator's own contribution, so the check never rejects a request that was + actually satisfiable; a comparer that collapses the effective domain below the + requested count is caught by the bounded draw instead. * **One principle, applied where its information exists.** Splitting the failure between declaration time (when the domain is countable) and generation time (when it is not) is not a dilution of the eager-conflict principle but its @@ -129,5 +138,5 @@ bounded-work principle. ## References * ADR-0011 — Host Dummies as a standalone package in this repository. -* The distinct-collection engine and the cardinality capability, in the `Dummies` - project (`CollectionState`, `ICardinalityHint`). +* The distinct-collection engine and the cardinality/membership capabilities, in the + `Dummies` project (`CollectionState`, `ICardinalityHint`, `IDomainMembership`). From 38b09ad812a0fe784441e0e00870b1241d5aed71 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 17:13:41 +0000 Subject: [PATCH 2/7] test(dummies): correct the dictionary-key coverage note in #188 tests The comment claimed the out-of-domain scenario was exercised "for dictionary-shaped keys", but no dictionary is constructed and none can be: AnyDictionary exposes no Containing surface, so its keys are gated purely by count. Say instead that dictionaries share the same CollectionState path and are therefore covered transitively. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- Dummies.UnitTests/AnyCollectionTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 24de9772..ff769e58 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -157,8 +157,10 @@ public void ContainingOutsideDomainExtendsCardinality() { Check.That(set.Count).IsEqualTo(3); } - // The same reasoning holds for a distinct list, for dictionary-shaped keys, and for several out-of-domain - // values at once — the correction applies wherever a distinct collection is gated by cardinality. + // The same reasoning holds for a distinct list and for several out-of-domain values at once. Dictionary keys + // run through the very same CollectionState path, so the correction reaches them too — but AnyDictionary has + // no Containing surface, so its keys are gated purely by count (see DictionaryOfBehaves) and cannot exercise + // the out-of-domain case directly. for (int i = 0; i < SampleCount; i++) { HashSet list = new(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate()); Check.That(list).Contains(1, 2, 3); From 5794263d6f7df67b894bdcddf533f18347e03d72 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 17:13:41 +0000 Subject: [PATCH 3/7] fix(dummies): gate distinct collections by the effective element domain A distinct collection (SetOf, ListOf(...).Distinct(), dictionary keys) gated its element count against the element generator's cardinality alone, so it rejected satisfiable requests whose Containing(...) values lie outside that domain -- e.g. Any.SetOf(Any.Int32().OneOf(1, 2)) .Containing(3).WithCount(3), where {1, 2, 3} is reachable. Model the effective distinct domain instead. A generator now answers both "how many distinct values can I produce" and "is this one of them" through a single unified interface ICardinalityHint (cardinality plus membership), so a fixed value proven outside the domain extends the effective cardinality while one already inside it does not. CollectionState counts only the elements that must come from the generator: it subtracts the contained values proven outside the domain (and each opaque ContainingAny draw) from the required count, and raises the resolution cap by the same out-of-domain values. Genuinely impossible requests still fail eagerly; an unprovable overlap defers to the bounded generation-time draw rather than a false conflict. The check is subtractive so it cannot overflow for a near-long.MaxValue domain, and membership is decided under the generator's own equality so it stays a sound upper bound under a custom comparer. Merging cardinality and membership into one interface makes the pairing compiler-enforced: a finite-cardinality generator cannot drift out of the eager perimeter without failing to compile. Hold that promise across the whole knowable perimeter by bringing every finite generator into it, including the six that silently sat outside before -- Int128, UInt128, Decimal, Double, Single and Half -- whose specs now advertise a finite cardinality (allow-lists, and narrow 128-bit ranges) and answer membership; continuum ranges and strings stay out, deferring to the bounded draw as before. Refine the still-Proposed ADR-0013 to the unified capability, and add ADR-0023 (Proposed) asking whether the exotic-width generators (Int128/UInt128 and their dedicated engine, and Half) should remain in a dummies library at all -- a breaking product decision left to the maintainer. Add tests across SetOf, ListOf().Distinct(), custom comparers, order-independent declaration, near-maximum cardinality, and the newly-gated decimal, floating-point and 128-bit generators. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- Dummies.UnitTests/AnyCollectionTests.cs | 33 ++++- Dummies/AnyBool.cs | 6 +- Dummies/AnyByte.cs | 6 +- Dummies/AnyChar.cs | 6 +- Dummies/AnyDateOnly.cs | 6 +- Dummies/AnyDateTime.cs | 6 +- Dummies/AnyDateTimeOffset.cs | 6 +- Dummies/AnyDecimal.cs | 6 +- Dummies/AnyDerivation.cs | 4 +- Dummies/AnyDouble.cs | 6 +- Dummies/AnyEnum.cs | 6 +- Dummies/AnyGuid.cs | 6 +- Dummies/AnyHalf.cs | 7 +- Dummies/AnyInt128.cs | 6 +- Dummies/AnyInt16.cs | 6 +- Dummies/AnyInt32.cs | 6 +- Dummies/AnyInt64.cs | 6 +- Dummies/AnySByte.cs | 6 +- Dummies/AnySingle.cs | 7 +- Dummies/AnyTimeOnly.cs | 6 +- Dummies/AnyTimeSpan.cs | 6 +- Dummies/AnyUInt128.cs | 6 +- Dummies/AnyUInt16.cs | 6 +- Dummies/AnyUInt32.cs | 6 +- Dummies/AnyUInt64.cs | 6 +- Dummies/CollectionState.cs | 15 +- Dummies/ContinuousIntervalSpec.cs | 19 +++ Dummies/DecimalIntervalSpec.cs | 18 +++ Dummies/ICardinalityHint.cs | 36 +++-- Dummies/IDomainMembership.cs | 37 ----- Dummies/OrdinalIntervalSpec.cs | 4 +- Dummies/WideIntervalSpec.cs | 27 ++++ ...ons-by-cardinality-else-bounded-draw.fr.md | 6 +- ...ctions-by-cardinality-else-bounded-draw.md | 5 +- ...-the-exotic-width-numeric-generators.fr.md | 134 ++++++++++++++++++ ...une-the-exotic-width-numeric-generators.md | 125 ++++++++++++++++ doc/handwritten/for-maintainers/adr/README.md | 1 + 37 files changed, 475 insertions(+), 129 deletions(-) delete mode 100644 Dummies/IDomainMembership.cs create mode 100644 doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md create mode 100644 doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index ff769e58..e4c7eea0 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -152,7 +152,7 @@ public void ContainingOutsideDomainExtendsCardinality() { // The motivating case: {1, 2, 3} is satisfiable — 3 is supplied directly and lies outside the {1, 2} the // generator can produce, so only two elements must be drawn from it. for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3); + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Generate(); Check.That(set).Contains(1, 2, 3); Check.That(set.Count).IsEqualTo(3); } @@ -165,7 +165,7 @@ public void ContainingOutsideDomainExtendsCardinality() { HashSet list = new(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate()); Check.That(list).Contains(1, 2, 3); - HashSet quad = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).Containing(4).WithCount(4); + HashSet quad = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).Containing(4).WithCount(4).Generate(); Check.That(quad).Contains(1, 2, 3, 4); Check.That(quad.Count).IsEqualTo(4); } @@ -183,7 +183,7 @@ public void ContainingInsideDomainDoesNotInflate() { // is exactly reachable as {1, 2, 5}. Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(4)).Throws(); for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(3); + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(3).Generate(); Check.That(set).Contains(1, 2, 5); } } @@ -209,7 +209,7 @@ public void ContainingNearMaximumCardinalityDoesNotOverflow() { // Between(0, long.MaxValue - 1) advertises long.MaxValue distinct values; the additive form base + extras // would overflow to a negative and reject spuriously. The subtractive check stays correct. for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int64().Between(0, long.MaxValue - 1)).Containing(-1L).WithCount(3); + HashSet set = Any.SetOf(Any.Int64().Between(0, long.MaxValue - 1)).Containing(-1L).WithCount(3).Generate(); Check.That(set).Contains(-1L); Check.That(set.Count).IsEqualTo(3); } @@ -220,7 +220,7 @@ public void ContainingAnyDefersToGeneration() { // The generator drawn from can yield a value outside the element domain, so the request cannot be proven // impossible at declaration — a wide ContainingAny makes it genuinely satisfiable. for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).ContainingAny(Any.Int32().GreaterThan(100)).WithCount(3); + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2)).ContainingAny(Any.Int32().GreaterThan(100)).WithCount(3).Generate(); Check.That(set.Count).IsEqualTo(3); Check.That(set).Contains(1, 2); } @@ -239,7 +239,7 @@ public void ContainingUnderAMergingComparer() { // 15 is outside {1, 2, 3} and its residue class (5) is fresh too, so {1, 2, 3, 15} has four classes and // generation succeeds. for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2, 3), modTen).Containing(15).WithCount(4); + HashSet set = Any.SetOf(Any.Int32().OneOf(1, 2, 3), modTen).Containing(15).WithCount(4).Generate(); Check.That(set.Count).IsEqualTo(4); } @@ -249,6 +249,27 @@ public void ContainingUnderAMergingComparer() { Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2), modTen).Containing(12).WithCount(3).Generate()).Throws(); } + [Fact(DisplayName = "The eager perimeter reaches every finite generator: decimal, floating-point and 128-bit allow-lists gate distinct collections too.")] + public void FiniteScalarGeneratorsGateEagerly() { + // A finite allow-list or a narrow range over decimal, double, single or Int128 now advertises its cardinality, + // so a count beyond it conflicts at declaration — the same promise integers and enums already kept, held + // across the whole knowable perimeter rather than only part of it. + Check.ThatCode(() => Any.SetOf(Any.Decimal().OneOf(1m, 2m)).WithCount(3)).Throws(); + Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).WithCount(3)).Throws(); + Check.ThatCode(() => Any.SetOf(Any.Single().OneOf(1f, 2f)).WithCount(3)).Throws(); + Check.ThatCode(() => Any.SetOf(Any.Int128().Between(1, 3)).WithCount(5)).Throws(); + + // Membership travels with cardinality: an out-of-domain contained value extends the effective domain... + for (int i = 0; i < SampleCount; i++) { + HashSet set = Any.SetOf(Any.Decimal().OneOf(1m, 2m)).Containing(3m).WithCount(3).Generate(); + Check.That(set).Contains(1m, 2m, 3m); + } + + // ...while a contained value already inside it does not, so an impossible count still conflicts eagerly. + Check.ThatCode(() => Any.SetOf(Any.Decimal().OneOf(1m, 2m)).Containing(1m).WithCount(3)).Throws(); + Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).Containing(2d).WithCount(3)).Throws(); + } + [Fact(DisplayName = "ArrayOf: produces an array of the requested size, distinct when asked.")] public void ArrayOfProducesArrays() { for (int i = 0; i < SampleCount; i++) { diff --git a/Dummies/AnyBool.cs b/Dummies/AnyBool.cs index e8acea5e..c1b44b85 100644 --- a/Dummies/AnyBool.cs +++ b/Dummies/AnyBool.cs @@ -5,7 +5,7 @@ namespace Dummies; /// mostly useful for symmetry when a test sweeps cases — and contradictory pins fail eagerly with a /// naming both sides, like every other generator. /// -public sealed class AnyBool : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyBool : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -36,10 +36,10 @@ private AnyBool(RandomSource source, bool? pinned, string? pinnedConstraint) { RandomSource? IHasRandomSource.Source => _source; // Two distinct values unless a pin has already fixed one of them. - long? ICardinalityHint.DistinctCardinality => _pinned is null ? 2 : 1; + long? ICardinalityHint.DistinctCardinality => _pinned is null ? 2 : 1; // A pin narrows the domain to that single value; unpinned, both booleans are producible. - bool IDomainMembership.Contains(bool value) => _pinned is not bool pinned || pinned == value; + bool ICardinalityHint.Contains(bool value) => _pinned is not bool pinned || pinned == value; /// Pins the value to true. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyByte.cs b/Dummies/AnyByte.cs index fc9bf97d..3fa12ccd 100644 --- a/Dummies/AnyByte.cs +++ b/Dummies/AnyByte.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnyByte(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(byte value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(byte value) => _spec.Contains(Ord(value)); /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyChar.cs b/Dummies/AnyChar.cs index a4ce3abd..b6e0dc7a 100644 --- a/Dummies/AnyChar.cs +++ b/Dummies/AnyChar.cs @@ -8,7 +8,7 @@ namespace Dummies; /// . A combination that empties the pool fails eagerly with a /// . /// -public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -63,10 +63,10 @@ private AnyChar(RandomSource source, RandomSource? IHasRandomSource.Source => _source; // The pool is materialized once at construction, so its size is the exact number of characters drawable. - long? ICardinalityHint.DistinctCardinality => _pool.Count; + long? ICardinalityHint.DistinctCardinality => _pool.Count; // The pool is the exact draw set, so membership is a direct pool lookup. - bool IDomainMembership.Contains(char value) => _pool.Contains(value); + bool ICardinalityHint.Contains(char value) => _pool.Contains(value); /// Restricts the character to ASCII letters only. Declared once per generator. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyDateOnly.cs b/Dummies/AnyDateOnly.cs index 0ca3839d..f613d8b4 100644 --- a/Dummies/AnyDateOnly.cs +++ b/Dummies/AnyDateOnly.cs @@ -16,7 +16,7 @@ namespace Dummies; /// constraint: a reproducible test pins its reference dates explicitly with and /// . /// -public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -56,9 +56,9 @@ private AnyDateOnly(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(DateOnly value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(DateOnly value) => _spec.Contains(Ord(value)); /// Requires a date strictly after . /// The exclusive lower bound. diff --git a/Dummies/AnyDateTime.cs b/Dummies/AnyDateTime.cs index 05a00658..89a5c495 100644 --- a/Dummies/AnyDateTime.cs +++ b/Dummies/AnyDateTime.cs @@ -19,7 +19,7 @@ namespace Dummies; /// comparison operators do. Values supplied to are returned as given, Kind included. There is deliberately no clock-relative constraint (no "in the past/future"): a /// reproducible test pins its reference instants explicitly with and . /// -public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -61,9 +61,9 @@ private AnyDateTime(RandomSource source, OrdinalIntervalSpec spec, IReadOnlyDict RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(DateTime value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(DateTime value) => _spec.Contains(Ord(value)); /// Requires an instant strictly after . /// The exclusive lower bound. diff --git a/Dummies/AnyDateTimeOffset.cs b/Dummies/AnyDateTimeOffset.cs index 7051c8bf..982fd2a3 100644 --- a/Dummies/AnyDateTimeOffset.cs +++ b/Dummies/AnyDateTimeOffset.cs @@ -21,7 +21,7 @@ namespace Dummies; /// past/future"): a reproducible test pins its reference instants explicitly with and /// . /// -public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -63,9 +63,9 @@ private AnyDateTimeOffset(RandomSource source, OrdinalIntervalSpec spec, IReadOn RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(DateTimeOffset value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(DateTimeOffset value) => _spec.Contains(Ord(value)); /// Requires an instant strictly after . /// The exclusive lower bound. diff --git a/Dummies/AnyDecimal.cs b/Dummies/AnyDecimal.cs index b8d93293..f90696fc 100644 --- a/Dummies/AnyDecimal.cs +++ b/Dummies/AnyDecimal.cs @@ -13,7 +13,7 @@ namespace Dummies; /// sides; instances are immutable recipes. Exclusive bounds are expressed as the inclusive bound plus a point /// exclusion, since has no next-representable-value ladder. /// -public sealed class AnyDecimal : IAny, IHasRandomSource { +public sealed class AnyDecimal : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -45,6 +45,10 @@ private AnyDecimal(RandomSource source, DecimalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + + bool ICardinalityHint.Contains(decimal value) => _spec.Contains(value); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyDerivation.cs b/Dummies/AnyDerivation.cs index 6663bdcb..a67d9d45 100644 --- a/Dummies/AnyDerivation.cs +++ b/Dummies/AnyDerivation.cs @@ -45,10 +45,10 @@ internal static class AnyDerivation { /// /// A conservative upper bound on the number of distinct values yields, when it - /// advertises one through ; null when the domain is unbounded or unknown. + /// advertises one through ; null when the domain is unbounded or unknown. /// internal static long? CardinalityOf(IAny generator) { - return (generator as ICardinalityHint)?.DistinctCardinality; + return (generator as ICardinalityHint)?.DistinctCardinality; } /// diff --git a/Dummies/AnyDouble.cs b/Dummies/AnyDouble.cs index 3c8f3586..2dadb182 100644 --- a/Dummies/AnyDouble.cs +++ b/Dummies/AnyDouble.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. /// -public sealed class AnyDouble : IAny, IHasRandomSource { +public sealed class AnyDouble : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -44,6 +44,10 @@ private AnyDouble(RandomSource source, ContinuousIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + + bool ICardinalityHint.Contains(double value) => _spec.Contains(value); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyEnum.cs b/Dummies/AnyEnum.cs index abde3bae..52cc6510 100644 --- a/Dummies/AnyEnum.cs +++ b/Dummies/AnyEnum.cs @@ -7,7 +7,7 @@ namespace Dummies; /// fails eagerly with a naming both sides. /// /// The enum type to draw values from. -public sealed class AnyEnum : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership +public sealed class AnyEnum : IAny, IHasRandomSource, ICardinalityHint where TEnum : struct, Enum { #region Statics members declarations @@ -59,10 +59,10 @@ private AnyEnum(RandomSource source, IReadOnlyList declared, RandomSource? IHasRandomSource.Source => _source; // The pool is materialized once at construction, so its size is the exact number of members drawable. - long? ICardinalityHint.DistinctCardinality => _pool.Count; + long? ICardinalityHint.DistinctCardinality => _pool.Count; // The pool is the exact draw set, so membership is a direct pool lookup. - bool IDomainMembership.Contains(TEnum value) => _pool.Contains(value); + bool ICardinalityHint.Contains(TEnum value) => _pool.Contains(value); /// Requires the value to be one of the supplied members. Declared once per generator. /// The allowed members; duplicates are ignored. Every value must be a declared member of — the generator never yields undeclared numeric values, not even explicitly supplied ones. diff --git a/Dummies/AnyGuid.cs b/Dummies/AnyGuid.cs index 5a67f79c..ef6742c6 100644 --- a/Dummies/AnyGuid.cs +++ b/Dummies/AnyGuid.cs @@ -8,7 +8,7 @@ namespace Dummies; /// to pin the empty identifier. Contradictory constraints fail eagerly with a /// naming both sides. /// -public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -66,10 +66,10 @@ private AnyGuid(RandomSource source, Guid? pinned, string? pinnedConstraint, RandomSource? IHasRandomSource.Source => _source; // Pinned to a single value, or bounded by an allow-list; otherwise the domain is effectively unbounded. - long? ICardinalityHint.DistinctCardinality => _pinned is not null ? 1 : _effectiveAllowed?.Count; + long? ICardinalityHint.DistinctCardinality => _pinned is not null ? 1 : _effectiveAllowed?.Count; // Mirrors Generate: the pin, then the allow-list, then the full space minus the exclusions. - bool IDomainMembership.Contains(Guid value) { + bool ICardinalityHint.Contains(Guid value) { if (_pinned is Guid pinned) { return pinned == value; } if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } diff --git a/Dummies/AnyHalf.cs b/Dummies/AnyHalf.cs index 21fc1cc3..e7efc5bc 100644 --- a/Dummies/AnyHalf.cs +++ b/Dummies/AnyHalf.cs @@ -14,7 +14,7 @@ namespace Dummies; /// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. Available on /// the net8.0 target only, like the type itself. /// -public sealed class AnyHalf : IAny, IHasRandomSource { +public sealed class AnyHalf : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -56,6 +56,11 @@ private AnyHalf(RandomSource source, ContinuousIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + + // The allow-list holds the doubles the supplied halves widen to, so membership tests the same widening. + bool ICardinalityHint.Contains(Half value) => _spec.Contains((double)value); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyInt128.cs b/Dummies/AnyInt128.cs index 1f040036..96eb0de5 100644 --- a/Dummies/AnyInt128.cs +++ b/Dummies/AnyInt128.cs @@ -14,7 +14,7 @@ namespace Dummies; /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// Available on the net8.0 target only, like the type itself. /// -public sealed class AnyInt128 : IAny, IHasRandomSource { +public sealed class AnyInt128 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -54,6 +54,10 @@ private AnyInt128(RandomSource source, WideIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + + bool ICardinalityHint.Contains(Int128 value) => _spec.Contains(Ord(value)); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyInt16.cs b/Dummies/AnyInt16.cs index a31187c2..11b141b8 100644 --- a/Dummies/AnyInt16.cs +++ b/Dummies/AnyInt16.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnyInt16(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(short value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(short value) => _spec.Contains(Ord(value)); /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyInt32.cs b/Dummies/AnyInt32.cs index 377b4a0a..188e1be0 100644 --- a/Dummies/AnyInt32.cs +++ b/Dummies/AnyInt32.cs @@ -28,7 +28,7 @@ namespace Dummies; /// /// /// -public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -68,9 +68,9 @@ private AnyInt32(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(int value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(int value) => _spec.Contains(Ord(value)); /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyInt64.cs b/Dummies/AnyInt64.cs index a4ef0337..2ad73622 100644 --- a/Dummies/AnyInt64.cs +++ b/Dummies/AnyInt64.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnyInt64(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(long value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(long value) => _spec.Contains(Ord(value)); /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. diff --git a/Dummies/AnySByte.cs b/Dummies/AnySByte.cs index 6a0fd6ee..0ef14ae0 100644 --- a/Dummies/AnySByte.cs +++ b/Dummies/AnySByte.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnySByte(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(sbyte value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(sbyte value) => _spec.Contains(Ord(value)); /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. diff --git a/Dummies/AnySingle.cs b/Dummies/AnySingle.cs index 499384ec..bede8742 100644 --- a/Dummies/AnySingle.cs +++ b/Dummies/AnySingle.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. /// -public sealed class AnySingle : IAny, IHasRandomSource { +public sealed class AnySingle : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -54,6 +54,11 @@ private AnySingle(RandomSource source, ContinuousIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + + // The allow-list holds the doubles the supplied floats widen to, so membership tests the same widening. + bool ICardinalityHint.Contains(float value) => _spec.Contains((double)value); + /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyTimeOnly.cs b/Dummies/AnyTimeOnly.cs index f7c0245b..c7f5d6b6 100644 --- a/Dummies/AnyTimeOnly.cs +++ b/Dummies/AnyTimeOnly.cs @@ -16,7 +16,7 @@ namespace Dummies; /// constraint: a reproducible test pins its reference time of days explicitly with and /// . /// -public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -56,9 +56,9 @@ private AnyTimeOnly(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(TimeOnly value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(TimeOnly value) => _spec.Contains(Ord(value)); /// Requires a time of day strictly after . /// The exclusive lower bound. diff --git a/Dummies/AnyTimeSpan.cs b/Dummies/AnyTimeSpan.cs index c6626258..69423a79 100644 --- a/Dummies/AnyTimeSpan.cs +++ b/Dummies/AnyTimeSpan.cs @@ -13,7 +13,7 @@ namespace Dummies; /// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one /// draw. Unconstrained, it draws from the full range, negative durations included. /// -public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -53,9 +53,9 @@ private AnyTimeSpan(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(TimeSpan value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(TimeSpan value) => _spec.Contains(Ord(value)); /// Requires a duration strictly greater than . /// A new generator carrying the added constraint. diff --git a/Dummies/AnyUInt128.cs b/Dummies/AnyUInt128.cs index d1617c1b..77be9bb6 100644 --- a/Dummies/AnyUInt128.cs +++ b/Dummies/AnyUInt128.cs @@ -14,7 +14,7 @@ namespace Dummies; /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// Available on the net8.0 target only, like the type itself. /// -public sealed class AnyUInt128 : IAny, IHasRandomSource { +public sealed class AnyUInt128 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -54,6 +54,10 @@ private AnyUInt128(RandomSource source, WideIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + + bool ICardinalityHint.Contains(UInt128 value) => _spec.Contains(Ord(value)); + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. diff --git a/Dummies/AnyUInt16.cs b/Dummies/AnyUInt16.cs index 4b2602f6..ce2a7b94 100644 --- a/Dummies/AnyUInt16.cs +++ b/Dummies/AnyUInt16.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnyUInt16(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(ushort value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(ushort value) => _spec.Contains(Ord(value)); /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyUInt32.cs b/Dummies/AnyUInt32.cs index ef535419..92f41085 100644 --- a/Dummies/AnyUInt32.cs +++ b/Dummies/AnyUInt32.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnyUInt32(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(uint value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(uint value) => _spec.Contains(Ord(value)); /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyUInt64.cs b/Dummies/AnyUInt64.cs index 4efb26ba..61b24eac 100644 --- a/Dummies/AnyUInt64.cs +++ b/Dummies/AnyUInt64.cs @@ -12,7 +12,7 @@ namespace Dummies; /// contradictory constraints fail eagerly with a naming both /// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. /// -public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint, IDomainMembership { +public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations @@ -52,9 +52,9 @@ private AnyUInt64(RandomSource source, OrdinalIntervalSpec spec) { RandomSource? IHasRandomSource.Source => _source; - long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; + long? ICardinalityHint.DistinctCardinality => _spec.Cardinality; - bool IDomainMembership.Contains(ulong value) => _spec.Contains(Ord(value)); + bool ICardinalityHint.Contains(ulong value) => _spec.Contains(Ord(value)); /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. diff --git a/Dummies/CollectionState.cs b/Dummies/CollectionState.cs index d78bcaa8..ddba71b9 100644 --- a/Dummies/CollectionState.cs +++ b/Dummies/CollectionState.cs @@ -16,9 +16,9 @@ namespace Dummies; /// /// /// Distinctness follows the two-layer contract the library commits to: when the element generator advertises a -/// small cardinality that the requested count would exceed — counting only +/// small cardinality that the requested count would exceed — counting only /// the elements that must be drawn from that generator, since values pinned with Containing(...) outside -/// its domain (see ) are supplied directly and extend the effective domain — +/// its domain (see ) are supplied directly and extend the effective domain — /// the conflict is caught at declaration time (); otherwise the /// count is drawn and the elements are filled by a bounded dedup-draw, and a genuine shortfall surfaces at /// generation as an naming the seed to replay. @@ -196,14 +196,15 @@ private void Validate(string applying) { private int FixedOutsideCount() { if (_fixedContaining.Count == 0) { return 0; } // A fixed value the element generator could never produce extends the effective distinct domain; a value - // already inside it does not. When the generator cannot answer membership (it advertises a cardinality but - // not a domain), stay conservative and treat every fixed value as outside: the eager check then never - // rejects a request that might be satisfiable, and a genuine shortfall is caught by the bounded draw. - if (_item is not IDomainMembership membership) { return _fixedContaining.Count; } + // already inside it does not. The cardinality snapshot came from this same generator, so whenever the eager + // check runs it also answers membership (cardinality and membership are one interface). The null branch is a + // defensive fallback: treat every fixed value as outside, so the check can only defer to the bounded draw, + // never falsely reject. + if (_item is not ICardinalityHint hint) { return _fixedContaining.Count; } int outside = 0; foreach (T value in _fixedContaining) { - if (!membership.Contains(value)) { outside++; } + if (!hint.Contains(value)) { outside++; } } return outside; diff --git a/Dummies/ContinuousIntervalSpec.cs b/Dummies/ContinuousIntervalSpec.cs index b03898e7..8aa1bd99 100644 --- a/Dummies/ContinuousIntervalSpec.cs +++ b/Dummies/ContinuousIntervalSpec.cs @@ -142,6 +142,25 @@ internal ContinuousIntervalSpec WithExcluded(double[] values, string applying) { return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); } + /// + /// The number of distinct values the specification can produce — the allow-list size when one is set, and + /// null otherwise: a floating-point continuum is not a countable domain, so it stays outside the eager + /// cardinality perimeter and a distinct collection over it falls back to the bounded draw. Feeds + /// . + /// + internal long? Cardinality => _effectiveAllowed?.Count; + + /// + /// Whether is one the specification could produce — a member of the allow-list when + /// one is set, otherwise inside the interval and not excluded. Non-finite inputs fall outside the bounds and + /// so return false. Mirrors 's own domain. + /// + internal bool Contains(double value) { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } + + return value >= _min && value <= _max && !IsExcluded(value); + } + /// Draws one value satisfying the whole specification. internal double Generate(Random random, int seed) { if (_effectiveAllowed is not null) { diff --git a/Dummies/DecimalIntervalSpec.cs b/Dummies/DecimalIntervalSpec.cs index 871a528e..dc1de5e6 100644 --- a/Dummies/DecimalIntervalSpec.cs +++ b/Dummies/DecimalIntervalSpec.cs @@ -107,6 +107,24 @@ internal DecimalIntervalSpec WithExcluded(decimal[] values, string applying) { return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); } + /// + /// The number of distinct values the specification can produce — the allow-list size when one is set, and + /// null otherwise: a interval is a countable domain in theory but astronomically + /// large, so it stays outside the eager cardinality perimeter and a distinct collection over it falls back to + /// the bounded draw. Feeds . + /// + internal long? Cardinality => _effectiveAllowed?.Count; + + /// + /// Whether is one the specification could produce — a member of the allow-list when + /// one is set, otherwise inside the interval and not excluded. Mirrors 's own domain. + /// + internal bool Contains(decimal value) { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(value); } + + return value >= _min && value <= _max && !IsExcluded(value); + } + /// Draws one value satisfying the whole specification. internal decimal Generate(Random random, int seed) { if (_effectiveAllowed is not null) { diff --git a/Dummies/ICardinalityHint.cs b/Dummies/ICardinalityHint.cs index e37d3a52..36e85a90 100644 --- a/Dummies/ICardinalityHint.cs +++ b/Dummies/ICardinalityHint.cs @@ -3,26 +3,36 @@ namespace Dummies; /// /// Implemented by the library's own generators that draw from a small, countable domain, so a distinct /// collection (, ListOf(...).Distinct(), a dictionary's keys) can tell — at -/// declaration time — that it is being asked for more distinct elements than the element generator could ever -/// produce, and fail eagerly with a instead of only discovering -/// it while drawing. +/// declaration time — whether a requested count, together with any values pinned through Containing(...), +/// can be satisfied from the effective domain, and fail eagerly with a +/// instead of only discovering it while drawing. /// /// -/// The value is a conservative upper bound on the number of distinct elements the generator yields. A -/// generator whose domain is unbounded, effectively unbounded, or simply unknown (a foreign -/// , a derived generator) does not implement this interface — the collection then relies on -/// the bounded dedup-draw fallback, which surfaces a genuine shortfall as an . -/// Because the bound ignores any custom (which can only merge values, -/// never create new ones), it stays a sound upper bound under a comparer too. +/// The two members travel together on purpose — that is the whole point of putting them on one interface: +/// answers "how many distinct values can the generator produce" (a +/// conservative upper bound), and answers "is this one of them". A distinct +/// collection needs both: the size to gate the count, and membership to tell a contained value that +/// extends the domain (one the generator could never draw) from one already inside it. Because they are a +/// single contract, a generator cannot advertise a cardinality without also answering membership — the compiler +/// keeps the promise, so no generator can drift out of the eager perimeter unnoticed. /// -/// A generator that advertises a finite cardinality here must also implement -/// , so a distinct collection can tell whether a value pinned with -/// Containing(...) extends the domain or already sits inside it; the two capabilities travel together. +/// A generator whose domain is unbounded, effectively unbounded, or simply unknown (a foreign +/// , a derived generator) does not implement this interface; the collection then relies +/// on the bounded dedup-draw fallback, which surfaces a genuine shortfall as an +/// . Because both the bound and membership ignore any custom +/// (which can only merge values, never create new ones), they stay +/// sound under a comparer too. /// /// -internal interface ICardinalityHint { +/// The element type. +internal interface ICardinalityHint { /// The number of distinct values the generator can produce, or null when that is unbounded or unknown. long? DistinctCardinality { get; } + /// Whether the generator, as constrained, could ever produce . + /// The candidate value. + /// true when is within the generator's domain; otherwise false. + bool Contains(T value); + } diff --git a/Dummies/IDomainMembership.cs b/Dummies/IDomainMembership.cs deleted file mode 100644 index a7e8c96c..00000000 --- a/Dummies/IDomainMembership.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Dummies; - -/// -/// Companion to : implemented by the library's own generators that draw from a -/// small, countable domain so a distinct collection can decide, at declaration time, whether a value -/// pinned with Containing(...) lies inside or outside the element generator's domain. A fixed value the -/// generator could never produce ( returns false) adds one to the effective -/// distinct cardinality — it is a value the generator itself cannot draw — while a value already in the domain -/// does not. See for how the effective cardinality gates satisfiability. -/// -/// -/// -/// The pairing is load-bearing: every generator that advertises a finite -/// must also implement this interface, and -/// must agree exactly with that finite domain — the same predicate the generator -/// honours when it draws. Cardinality answers "how many", membership answers "which". A generator whose -/// domain is unbounded or unknown implements neither; the collection then relies on the bounded dedup-draw -/// fallback. Should a finite-cardinality generator ever omit this interface, -/// stays safe by treating every contained value as outside the domain — conservative, so the eager check -/// never rejects a request that might be satisfiable. -/// -/// -/// Membership is decided under the generator's own (default) equality. A custom collection -/// can only merge values, never create new ones, so a value that -/// is "provably outside" under default equality remains a sound upper-bound contribution to the effective -/// cardinality; a comparer that collapses it back into the domain is caught by the bounded draw instead. -/// -/// -/// The element type. -internal interface IDomainMembership { - - /// Whether the generator, as constrained, could ever produce . - /// The candidate value. - /// true when is within the generator's domain; otherwise false. - bool Contains(T value); - -} diff --git a/Dummies/OrdinalIntervalSpec.cs b/Dummies/OrdinalIntervalSpec.cs index 93aa4a06..437d20ed 100644 --- a/Dummies/OrdinalIntervalSpec.cs +++ b/Dummies/OrdinalIntervalSpec.cs @@ -149,7 +149,7 @@ internal OrdinalIntervalSpec WithExcluded(ulong[] ordinals, string applying) { /// /// The number of distinct values the specification can produce, or null when that exceeds /// (a range too wide to ever conflict with a collection count). Feeds - /// , so a distinct collection over a narrow integer range can fail eagerly. + /// , so a distinct collection over a narrow integer range can fail eagerly. /// internal long? Cardinality { get { @@ -165,7 +165,7 @@ internal long? Cardinality { /// /// Whether is a value the specification could produce — the exact domain /// draws from: a member of the allow-list when one is set, otherwise inside - /// the interval and not excluded. Feeds , so a distinct collection can tell + /// the interval and not excluded. Feeds , so a distinct collection can tell /// a contained value that extends the domain from one already inside it. /// internal bool Contains(ulong ordinal) { diff --git a/Dummies/WideIntervalSpec.cs b/Dummies/WideIntervalSpec.cs index 3ee476a3..b297ca16 100644 --- a/Dummies/WideIntervalSpec.cs +++ b/Dummies/WideIntervalSpec.cs @@ -121,6 +121,33 @@ internal WideIntervalSpec WithExcluded(UInt128[] ordinals, string applying) { return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); } + /// + /// The number of distinct values the specification can produce, or null when the interval is full-width + /// or wider than (a range too vast to ever conflict with a collection count). + /// Feeds , so a distinct collection over a narrow 128-bit range or allow-list + /// can fail eagerly. + /// + internal long? Cardinality { + get { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } + if (IsFullWidth()) { return null; } + + UInt128 count = _max - _min + 1 - (UInt128)_excludedInRange.Count; + + return count <= (UInt128)long.MaxValue ? (long)count : null; + } + } + + /// + /// Whether is a value the specification could produce — the exact domain + /// draws from. Feeds . + /// + internal bool Contains(UInt128 ordinal) { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Contains(ordinal); } + + return ordinal >= _min && ordinal <= _max && !_excludedInRange.Contains(ordinal); + } + /// Draws one ordinal satisfying the whole specification — built directly, never generate-then-retry. internal UInt128 GenerateOrdinal(Random random) { if (_effectiveAllowed is not null) { diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md index 2f3c9b7f..f6271e3c 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md @@ -150,6 +150,6 @@ principe de travail borné de la bibliothèque. ## Références * ADR-0011 — Héberger Dummies comme un paquet autonome dans ce dépôt. -* Le moteur de collection distincte et les capacités de cardinalité et - d'appartenance, dans le projet `Dummies` (`CollectionState`, `ICardinalityHint`, - `IDomainMembership`). +* Le moteur de collection distincte et sa capacité unifiée de cardinalité et + d'appartenance (une seule interface, pour qu'un générateur fini ne puisse pas sortir + du périmètre anticipé), dans le projet `Dummies` (`CollectionState`, `ICardinalityHint`). diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md index 6b4467ab..3646f969 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md @@ -138,5 +138,6 @@ bounded-work principle. ## References * ADR-0011 — Host Dummies as a standalone package in this repository. -* The distinct-collection engine and the cardinality/membership capabilities, in the - `Dummies` project (`CollectionState`, `ICardinalityHint`, `IDomainMembership`). +* The distinct-collection engine and its unified cardinality-and-membership capability + (one interface, so a finite generator cannot drift out of the eager perimeter), in the + `Dummies` project (`CollectionState`, `ICardinalityHint`). diff --git a/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md b/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md new file mode 100644 index 00000000..f6efd662 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md @@ -0,0 +1,134 @@ +# ADR-0023 | Élaguer les générateurs numériques de largeur exotique (128 bits et Half) + +🌍 🇬🇧 [English](0023-prune-the-exotic-width-numeric-generators.md) · 🇫🇷 Français (ce fichier) + +**Statut :** Proposé +**Date :** 2026-07-19 +**Décideurs :** Reefact + +## Contexte + +`Dummies` fournit une large matrice numérique pour qu'un test puisse demander une +valeur arbitraire de presque tout type de nombre intégré : `Byte`/`SByte`, les +entiers 16/32/64 bits, `Int128`/`UInt128`, `Decimal` et +`Double`/`Single`/`Half`. Le but est de produire des dummies *facilement* — +couvrir tous les cas simples et une majorité des cas complexes plausibles — sans +devenir un solveur de contraintes. + +Deux de ces types portent un coût disproportionné par rapport à leur usage réel : + +* **`Int128`/`UInt128`** reposent sur leur propre moteur, `WideIntervalSpec` — + un clone `UInt128` d'~185 lignes d'`OrdinalIntervalSpec`, le *quatrième* + moteur d'intervalle de la bibliothèque, n'existant que pour ces deux + générateurs. Un dummy 128 bits *contraint* (une plage bornée, une liste + blanche, une exclusion) est une fraction négligeable de l'usage réel ; le type + est net8 uniquement, donc même pas à portée d'un débutant. +* **`Half`** repose sur le moteur continu partagé `ContinuousIntervalSpec` : il + ne coûte donc aucun moteur — seulement ~207 lignes de surface — mais un dummy + `Half` est un besoin rare (interop ML/GPU). + +Le coût est devenu concret en étendant le périmètre anticipé de cardinalité pour +que *tout* générateur à domaine fini contrôle les collections distinctes de façon +uniforme (le travail « tenir la promesse partout » derrière +[ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md)). +En les rentrant dans le périmètre, on a découvert que `WideIntervalSpec` avait +déjà **silencieusement dérivé** hors de la capacité de cardinalité antérieure — +il n'annonçait ni cardinalité ni appartenance — donc le clone était une dette de +maintenance vivante, et pas seulement des lignes dormantes. Chaque moteur +d'intervalle conservé est un endroit où le prochain invariant transverse devra +être tissé à la main. + +Retirer des types de générateurs publics est un **changement cassant** qui touche +le socle de types supportés : c'est donc une décision produit délibérée, pas un +nettoyage de code — d'où cet ADR. + +## Décision + +Retirer `AnyInt128`, `AnyUInt128` et le moteur `WideIntervalSpec` qui n'existe +que pour eux ; et retirer `AnyHalf`. Conserver toute la matrice entière jusqu'à +64 bits et les générateurs flottants `Decimal`/`Double`/`Single` — la largeur que +l'on utilise réellement. Un test ayant besoin d'un dummy 128 bits ou `Half` +contraint utilise un littéral, ou un cast depuis le générateur immédiatement plus +large (`(Half)Any.Single().Between(...)`, un cast `Int64`). + +## Justification + +* **Le périmètre, pas l'exhaustivité.** Une bibliothèque de dummies vaut par la + trivialité des cas courants et la facilité de la majorité des cas complexes — + non par la couverture de tout type numérique du BCL à la même profondeur. + `Int128`/`UInt128`/`Half` sont la traîne où la demande de dummy contraint tend + vers zéro. +* **Un moteur de moins à tenir honnête.** Supprimer `WideIntervalSpec` retire un + moteur d'intervalle entier — un vrai concept, pas seulement des lignes — et + avec lui l'un des quatre endroits où chaque futur invariant transverse + (cardinalité, appartenance, une future capacité) devrait être ré-implémenté et + maintenu synchronisé. La dérive déjà observée en est la preuve. +* **L'échappatoire est bon marché.** La régression est bornée et locale : un cast + ou un littéral au point d'appel couvre tous les cas que servaient les + générateurs retirés, donc aucun test réaliste ne perd une capacité qu'il ne + puisse trivialement retrouver. + +## Alternatives considérées + +### Tout garder en l'état + +Le statu quo. Rejeté parce qu'il préserve un moteur entier et ~560 lignes pour un +bénéfice réel quasi nul et que — comme l'a montré la dérive silencieuse de +`WideIntervalSpec` — cette surface pourrit activement, taxant chaque futur +invariant. + +### Ne couper que les générateurs 128 bits, garder `Half` + +Défendable : `Half` repose sur le moteur continu partagé, donc il ne coûte aucun +*concept*, seulement des lignes, et notre étalon pondéré par les concepts traite +la largeur lisible sur un moteur partagé comme quasi gratuite (le même +raisonnement qui garde `Byte`…`UInt64`). C'est le repli naturel si `Half` s'avère +avoir de vrais utilisateurs ; la coupe 128 bits (qui retire le moteur) est la +partie porteuse. + +### Unifier les moteurs d'intervalle au lieu d'élaguer + +Refondre `WideIntervalSpec` dans un moteur d'intervalle générique partagé avec +`OrdinalIntervalSpec`. Rejeté : la generic math (`INumber`) est net8 +uniquement, donc sur le socle .NET Standard 2.0 il n'y a aucune abstraction +partagée *vers laquelle* unifier — les options honnêtes sont « garder le clone » +ou « retirer les types », pas « fusionner ». + +## Conséquences + +### Positives + +* L'un des quatre moteurs d'intervalle disparaît ; l'invariant + cardinalité/appartenance (et tout futur invariant) a moins de générateurs à + atteindre. +* La surface de la bibliothèque suit de plus près l'usage réel des dummies. + +### Négatives + +* Un changement cassant pour tout appelant de + `Any.Int128()`/`Any.UInt128()`/`Any.Half()` — atténué par un cast ou un + littéral au point d'appel, et signalé dans les notes de version. +* Perte d'uniformité de la matrice numérique : la liste des types ne reflète plus + un pour un les types numériques du BCL. + +### Risques + +* **Demande sous-estimée** — si les dummies 128 bits ou `Half` contraints + s'avèrent plus utilisés que prévu, la coupe est réversible (les générateurs + sont mécaniques), mais au prix du remaniement que cet ADR cherche à éviter. + Atténué en livrant le retrait dans une version majeure clairement signalée. + +## Actions de suivi + +* À l'acceptation, retirer les générateurs, `WideIntervalSpec`, leurs entrées de + fabrique `Any.*` et leurs tests ; signaler le changement cassant dans les notes + de version. +* Si seule la coupe 128 bits est acceptée (`Half` conservé), laisser `AnyHalf` + intact sur le moteur partagé. + +## Références + +* [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) — Héberger Dummies comme un paquet autonome. +* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md) — le périmètre anticipé de cardinalité dans lequel ces types ont été rentrés. +* Les générateurs et le moteur proposés au retrait, dans le projet `Dummies` + (`AnyInt128`, `AnyUInt128`, `WideIntervalSpec`, `AnyHalf`). diff --git a/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md b/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md new file mode 100644 index 00000000..835e5109 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md @@ -0,0 +1,125 @@ +# ADR-0023 | Prune the exotic-width numeric generators (128-bit and Half) + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0023-prune-the-exotic-width-numeric-generators.fr.md) + +**Status:** Proposed +**Date:** 2026-07-19 +**Decision Makers:** Reefact + +## Context + +`Dummies` ships a wide numeric matrix so that a test can ask for an arbitrary +value of almost any built-in number type: `Byte`/`SByte`, the 16/32/64-bit +integers, `Int128`/`UInt128`, `Decimal`, and `Double`/`Single`/`Half`. The goal +is producing dummies *easily* — cover every simple case and a majority of the +plausible complex ones — without becoming a constraint solver. + +Two of these types carry a cost out of proportion to their realistic use: + +* **`Int128`/`UInt128`** are backed by their own spec engine, + `WideIntervalSpec` — a ~185-line `UInt128` clone of `OrdinalIntervalSpec`, + the library's *fourth* interval engine, existing only to serve these two + generators. A constrained 128-bit dummy (a bounded range, an allow-list, an + exclusion) is a rounding-error fraction of real usage; the type is net8-only, + so it is not even a beginner's reach. +* **`Half`** rides the shared `ContinuousIntervalSpec`, so it costs no engine — + only ~207 lines of generator surface — but a `Half` dummy is a rare + ML/GPU-interop need. + +The cost became concrete while extending the eager cardinality perimeter so +that *every* finite-domain generator gates distinct collections uniformly (the +"hold the promise everywhere" work behind [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md)). +Bringing these types into the perimeter revealed that `WideIntervalSpec` had +already **silently drifted** out of the earlier cardinality capability — it +advertised neither a cardinality nor membership — so the clone was a live +maintenance liability, not merely dormant lines. Every interval engine we keep +is a place the next cross-cutting invariant must be threaded by hand. + +Removing public generator types is a **breaking change** and touches the +supported-type floor, so it is a deliberate product decision, not a code +cleanup — hence this ADR. + +## Decision + +Remove `AnyInt128`, `AnyUInt128`, and the `WideIntervalSpec` engine that exists +only for them; and remove `AnyHalf`. Keep the full integer matrix up to 64 bits +and the `Decimal`/`Double`/`Single` floating-point generators — the breadth +people actually reach for. A test needing a constrained 128-bit or `Half` dummy +uses a literal, or a cast from the next-widest generator +(`(Half)Any.Single().Between(...)`, an `Int64` cast). + +## Rationale + +* **Scope, not completeness.** A dummies library earns its keep by making the + common cases trivial and the majority of complex ones easy — not by covering + every BCL numeric type to the same depth. `Int128`/`UInt128`/`Half` are the + tail where constrained-dummy demand rounds to zero. +* **One fewer engine to keep honest.** Deleting `WideIntervalSpec` removes an + entire interval engine — a real concept, not just lines — and with it one of + the four places every future cross-cutting invariant (cardinality, membership, + a future capability) would have to be re-implemented and kept in sync. The + already-observed drift is the evidence. +* **The escape hatch is cheap.** The regression is bounded and local: a cast or + a literal at the call site covers every case the removed generators served, so + no realistic test loses a capability it cannot trivially recover. + +## Alternatives Considered + +### Keep everything as-is + +The status quo. Rejected because it preserves a whole engine and ~560 lines for +near-null realistic benefit, and — as the silent `WideIntervalSpec` drift +showed — that surface actively rots, taxing every future invariant. + +### Cut only the 128-bit generators, keep `Half` + +Tenable: `Half` rides the shared continuous engine, so it costs no *concept*, +only lines, and our concept-weighted yardstick treats legible breadth on a +shared engine as nearly free (the same reasoning that keeps `Byte`…`UInt64`). +This is the natural fallback if `Half` turns out to have real users; the 128-bit +cut (which removes the engine) is the load-bearing part. + +### Unify the interval engines instead of pruning + +Fold `WideIntervalSpec` back into a generic-math interval engine shared with +`OrdinalIntervalSpec`. Rejected: generic-math (`INumber`) is net8-only, so on +the .NET Standard 2.0 floor there is no shared abstraction to unify *into* — +the honest options are "keep the clone" or "drop the types", not "merge". + +## Consequences + +### Positive + +* One of the four interval engines disappears; the cardinality/membership + invariant (and any future one) has fewer generators to reach. +* The library's surface tracks realistic dummy usage more closely. + +### Negative + +* A breaking change for any caller using `Any.Int128()`/`Any.UInt128()`/`Any.Half()` + — mitigated by a cast or literal at the call site, and called out in the + release notes. +* Loss of uniformity in the numeric matrix: the type list no longer mirrors the + BCL's numeric types one-for-one. + +### Risks + +* **Underestimated demand** — if constrained 128-bit or `Half` dummies turn out + to be used more than expected, the cut is reversible (the generators are + mechanical), but at the cost of the churn this ADR is trying to avoid. + Mitigated by shipping the removal in a clearly-noted major version. + +## Follow-up Actions + +* On acceptance, remove the generators, `WideIntervalSpec`, their `Any.*` + factory entries, and their tests; note the breaking change in the release + notes. +* If only the 128-bit cut is accepted (Half kept), leave `AnyHalf` on the shared + engine untouched. + +## References + +* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) — Host Dummies as a standalone package. +* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) — the eager cardinality perimeter these types were brought into. +* The generators and engine proposed for removal, in the `Dummies` project + (`AnyInt128`, `AnyUInt128`, `WideIntervalSpec`, `AnyHalf`). diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 1ee0d4eb..63a224d3 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -196,3 +196,4 @@ Optional supporting material: | [ADR-0020](0020-materialize-dummies-only-through-generate.md) | Materialize dummies only through Generate() | Accepted | | [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) | Bind out-of-DTO arguments as peers through a source-agnostic untyped entry | Proposed | | [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) | Floor the library's .NET Framework support at 4.7.2 | Accepted | +| [ADR-0023](0023-prune-the-exotic-width-numeric-generators.md) | Prune the exotic-width numeric generators (128-bit and Half) | Proposed | From 46454debd7463d16743b9b4f28cd82ce26ae18e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 21:29:22 +0000 Subject: [PATCH 4/7] fix(dummies): report a pinned decimal or float domain as finite DecimalIntervalSpec and ContinuousIntervalSpec only reported a finite cardinality for their allow-list branch, so a validated pin such as Any.Decimal().Zero() or Any.Double().Between(1, 1) -- a singleton domain of exactly one value -- was left outside the eager perimeter: Any.SetOf(Any.Decimal().Zero()).WithCount(2) passed declaration and failed only later during the bounded draw. Report 1 for a validated pin (_min == _max) so the fully knowable contradiction conflicts eagerly, and cover it with a distinct-collection test. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- Dummies.UnitTests/AnyCollectionTests.cs | 18 ++++++++++++++++++ Dummies/ContinuousIntervalSpec.cs | 18 +++++++++++++----- Dummies/DecimalIntervalSpec.cs | 18 +++++++++++++----- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index e4c7eea0..10d24726 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -270,6 +270,24 @@ public void FiniteScalarGeneratorsGateEagerly() { Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).Containing(2d).WithCount(3)).Throws(); } + [Fact(DisplayName = "A validated pin is a singleton domain: a distinct collection asking for more than one conflicts eagerly.")] + public void SingletonScalarDomainsGateEagerly() { + // Zero()/Between(x, x) pins the domain to a single value; asking a distinct collection for two is a fully + // knowable contradiction, so it must fail at declaration, not only while drawing. + Check.ThatCode(() => Any.SetOf(Any.Decimal().Zero()).WithCount(2)).Throws(); + Check.ThatCode(() => Any.SetOf(Any.Double().Between(1d, 1d)).WithCount(2)).Throws(); + Check.ThatCode(() => Any.SetOf(Any.Single().Zero()).WithCount(2)).Throws(); + + // The singleton still generates at count one, and an out-of-domain contained value extends it as usual. + for (int i = 0; i < SampleCount; i++) { + HashSet one = Any.SetOf(Any.Decimal().Zero()).WithCount(1).Generate(); + Check.That(one).ContainsExactly(0m); + + HashSet two = Any.SetOf(Any.Decimal().Zero()).Containing(5m).WithCount(2).Generate(); + Check.That(two).Contains(0m, 5m); + } + } + [Fact(DisplayName = "ArrayOf: produces an array of the requested size, distinct when asked.")] public void ArrayOfProducesArrays() { for (int i = 0; i < SampleCount; i++) { diff --git a/Dummies/ContinuousIntervalSpec.cs b/Dummies/ContinuousIntervalSpec.cs index 8aa1bd99..a4503970 100644 --- a/Dummies/ContinuousIntervalSpec.cs +++ b/Dummies/ContinuousIntervalSpec.cs @@ -143,12 +143,20 @@ internal ContinuousIntervalSpec WithExcluded(double[] values, string applying) { } /// - /// The number of distinct values the specification can produce — the allow-list size when one is set, and - /// null otherwise: a floating-point continuum is not a countable domain, so it stays outside the eager - /// cardinality perimeter and a distinct collection over it falls back to the bounded draw. Feeds - /// . + /// The number of distinct values the specification can produce — the allow-list size when one is set, 1 + /// for a validated pin (_min == _max, a singleton domain), and null otherwise: a floating-point + /// range is treated as a continuum (counting its representable values is a type-specific concern the shared + /// engine does not carry), so it stays outside the eager cardinality perimeter and a distinct collection over + /// it falls back to the bounded draw. Feeds . /// - internal long? Cardinality => _effectiveAllowed?.Count; + internal long? Cardinality { + get { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } + if (_min == _max) { return 1; } + + return null; + } + } /// /// Whether is one the specification could produce — a member of the allow-list when diff --git a/Dummies/DecimalIntervalSpec.cs b/Dummies/DecimalIntervalSpec.cs index dc1de5e6..a0e888a5 100644 --- a/Dummies/DecimalIntervalSpec.cs +++ b/Dummies/DecimalIntervalSpec.cs @@ -108,12 +108,20 @@ internal DecimalIntervalSpec WithExcluded(decimal[] values, string applying) { } /// - /// The number of distinct values the specification can produce — the allow-list size when one is set, and - /// null otherwise: a interval is a countable domain in theory but astronomically - /// large, so it stays outside the eager cardinality perimeter and a distinct collection over it falls back to - /// the bounded draw. Feeds . + /// The number of distinct values the specification can produce — the allow-list size when one is set, 1 + /// for a validated pin (_min == _max, a singleton domain), and null otherwise: a wider + /// interval is a countable domain in theory but astronomically large, so it stays + /// outside the eager cardinality perimeter and a distinct collection over it falls back to the bounded draw. + /// Feeds . /// - internal long? Cardinality => _effectiveAllowed?.Count; + internal long? Cardinality { + get { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; } + if (_min == _max) { return 1; } + + return null; + } + } /// /// Whether is one the specification could produce — a member of the allow-list when From e0f400aee1e727ed61649d93f8e3c6c3bab59f1c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 21:34:54 +0000 Subject: [PATCH 5/7] docs(dummies): state the eager-cardinality boundary in ADR-0013 Codex noted that finite floating-point ranges (a narrow Half/Single sub-range, Half's ~63k full domain) sit outside the eager perimeter. That is intentional (Option A): counting a narrow float type's representable values in a range is type-specific bit-arithmetic disproportionate to the dummy use case, the case is pathological, and the bounded seed-named draw already covers it. Make the boundary explicit in ADR-0013's two-groups paragraph (EN + FR): pools, narrow integer/time ranges, allow-lists and single-value pins are counted cheaply and gated eagerly; a floating-point range defers, so a decimal or floating-point generator is gated only through an allow-list or a pin. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- ...ons-by-cardinality-else-bounded-draw.fr.md | 22 ++++++++++++------- ...ctions-by-cardinality-else-bounded-draw.md | 15 ++++++++----- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md index f6271e3c..88f65bd3 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md @@ -25,14 +25,20 @@ seul générateur d'éléments ne borne donc que les éléments qui doivent veni lui*, non la demande entière. Les générateurs d'éléments se répartissent en deux groupes. Certains tirent d'un -domaine petit et dénombrable : un booléen a deux valeurs, une énumération a ses -membres déclarés, un intervalle entier étroit ou un pool de caractères restreint a -une taille fixe. D'autres tirent d'un domaine effectivement non borné ou -simplement inconnaissable pour la collection : entiers non contraints, chaînes et -identifiants, et — de façon décisive — toute implémentation étrangère de -`IAny` ou tout générateur dérivé (`As`, `Combine`), qui ne porte aucune -information de domaine. `IAny` est une interface publique : la bibliothèque ne -peut donc pas supposer que tout générateur sache rapporter sa cardinalité. +domaine que la bibliothèque sait compter **à bas coût** : les deux valeurs d'un +booléen, les membres déclarés d'une énumération, un intervalle entier ou temporel +étroit, un pool de caractères restreint, une liste blanche explicite (`OneOf`), ou +un scalaire épinglé sur une seule valeur (`Zero`, `Between(x, x)`). D'autres tirent +d'un domaine effectivement non borné, ou dénombrable seulement en principe à un +coût que la bibliothèque refuse de payer : entiers non contraints, chaînes et +identifiants, une **plage** flottante (finie en valeurs représentables, mais les +compter relève d'une arithmétique de bits spécifique au type, disproportionnée pour +l'usage « dummy » — un générateur décimal ou flottant n'est donc contrôlé que via +une liste blanche ou un pin, jamais une plage plus large), et — de façon décisive — +toute implémentation étrangère de `IAny` ou tout générateur dérivé (`As`, +`Combine`), qui ne porte aucune information de domaine. `IAny` est une interface +publique : la bibliothèque ne peut donc pas supposer que tout générateur sache +rapporter sa cardinalité. Un comparateur d'égalité personnalisé ne peut que fusionner des valeurs distinctes en un nombre moindre de classes d'équivalence ; il ne peut jamais en créer de diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md index 3646f969..74555a0c 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md @@ -23,11 +23,16 @@ could never draw — and by the opaque draws of `ContainingAny(...)`. The elemen generator's cardinality alone therefore bounds only the elements that must come *from* it, not the whole request. -Element generators fall into two groups. Some draw from a small, countable -domain: a boolean has two values, an enum has its declared members, a narrow -integer range or a restricted character pool has a fixed size. Others draw from a -domain that is effectively unbounded or simply unknowable to the collection: -unconstrained integers, strings and identifiers, and — decisively — any foreign +Element generators fall into two groups. Some draw from a domain the library can +count **cheaply**: a boolean's two values, an enum's declared members, a narrow +integer or time range, a restricted character pool, an explicit allow-list +(`OneOf`), or a scalar pinned to a single value (`Zero`, `Between(x, x)`). Others +draw from a domain that is effectively unbounded, or countable only in principle +at a cost the library declines to pay: unconstrained integers, strings and +identifiers, a floating-point **range** (finite in representable values, but +counting them is type-specific bit-arithmetic disproportionate to the dummy use +case — so a decimal or floating-point generator is gated only through an +allow-list or a pin, never a wider range), and — decisively — any foreign `IAny` implementation or any derived generator (`As`, `Combine`), which carries no domain information at all. `IAny` is a public interface, so the library cannot assume every generator can report its cardinality. From c203ba042838df1db1788bd5c260e56cb6fcf1bc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 21:46:53 +0000 Subject: [PATCH 6/7] docs(dummies): mark ADR-0013 as accepted Flip ADR-0013 (gate distinct collections by cardinality, otherwise by a bounded draw) from Proposed to Accepted at the maintainer's decision, and set its date to the day it reached that status, per the index convention. Update the ADR index row to match. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- ...istinct-collections-by-cardinality-else-bounded-draw.fr.md | 4 ++-- ...e-distinct-collections-by-cardinality-else-bounded-draw.md | 4 ++-- doc/handwritten/for-maintainers/adr/README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md index 88f65bd3..1bee6278 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md @@ -2,8 +2,8 @@ 🌍 🇬🇧 [English](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé -**Date :** 2026-07-18 +**Statut :** Accepté +**Date :** 2026-07-19 **Décideurs :** Reefact ## Contexte diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md index 74555a0c..f28c63ea 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md @@ -2,8 +2,8 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md) -**Status:** Proposed -**Date:** 2026-07-18 +**Status:** Accepted +**Date:** 2026-07-19 **Decision Makers:** Reefact ## Context diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 63a224d3..6a33cdd0 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -186,7 +186,7 @@ Optional supporting material: | [ADR-0010](0010-treat-gendocs-error-catalog-as-a-versioned-contract.md) | Treat GenDoc's error catalog as a versioned contract | Accepted | | [ADR-0011](0011-host-dummies-as-a-standalone-package.md) | Host Dummies as a standalone package in this repository | Proposed | | [ADR-0012](0012-fix-the-binder-options-before-binding-begins.md) | Fix the binder options before binding begins | Accepted | -| [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) | Gate distinct collections by cardinality, otherwise by a bounded draw | Proposed | +| [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) | Gate distinct collections by cardinality, otherwise by a bounded draw | Accepted | | [ADR-0014](0014-bind-a-required-list-by-presence-not-cardinality.md) | Bind a required list by presence, not cardinality | Accepted | | [ADR-0015](0015-cap-any-combine-at-arity-eight.md) | Cap Any.Combine at arity eight | Proposed | | [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.md) | Make the binder's structural error codes configurable | Superseded | From 5a25cb7dd82ca3be536319c477f2dd4b9253810b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 21:53:25 +0000 Subject: [PATCH 7/7] docs(dummies): drop ADR-0023 draft, keep 128-bit and Half dummies The maintainer decided to keep Any.Int128, Any.UInt128, and Any.Half rather than prune them, preserving the "Any covers every numeric primitive" orthogonality. The proposal never reached main, so remove the never-accepted draft and its index row; no generator code changes. Refs: #188 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01L1JGj1wj5C55UE9vxQFWoF --- ...-the-exotic-width-numeric-generators.fr.md | 134 ------------------ ...une-the-exotic-width-numeric-generators.md | 125 ---------------- doc/handwritten/for-maintainers/adr/README.md | 1 - 3 files changed, 260 deletions(-) delete mode 100644 doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md delete mode 100644 doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md diff --git a/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md b/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md deleted file mode 100644 index f6efd662..00000000 --- a/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.fr.md +++ /dev/null @@ -1,134 +0,0 @@ -# ADR-0023 | Élaguer les générateurs numériques de largeur exotique (128 bits et Half) - -🌍 🇬🇧 [English](0023-prune-the-exotic-width-numeric-generators.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Proposé -**Date :** 2026-07-19 -**Décideurs :** Reefact - -## Contexte - -`Dummies` fournit une large matrice numérique pour qu'un test puisse demander une -valeur arbitraire de presque tout type de nombre intégré : `Byte`/`SByte`, les -entiers 16/32/64 bits, `Int128`/`UInt128`, `Decimal` et -`Double`/`Single`/`Half`. Le but est de produire des dummies *facilement* — -couvrir tous les cas simples et une majorité des cas complexes plausibles — sans -devenir un solveur de contraintes. - -Deux de ces types portent un coût disproportionné par rapport à leur usage réel : - -* **`Int128`/`UInt128`** reposent sur leur propre moteur, `WideIntervalSpec` — - un clone `UInt128` d'~185 lignes d'`OrdinalIntervalSpec`, le *quatrième* - moteur d'intervalle de la bibliothèque, n'existant que pour ces deux - générateurs. Un dummy 128 bits *contraint* (une plage bornée, une liste - blanche, une exclusion) est une fraction négligeable de l'usage réel ; le type - est net8 uniquement, donc même pas à portée d'un débutant. -* **`Half`** repose sur le moteur continu partagé `ContinuousIntervalSpec` : il - ne coûte donc aucun moteur — seulement ~207 lignes de surface — mais un dummy - `Half` est un besoin rare (interop ML/GPU). - -Le coût est devenu concret en étendant le périmètre anticipé de cardinalité pour -que *tout* générateur à domaine fini contrôle les collections distinctes de façon -uniforme (le travail « tenir la promesse partout » derrière -[ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md)). -En les rentrant dans le périmètre, on a découvert que `WideIntervalSpec` avait -déjà **silencieusement dérivé** hors de la capacité de cardinalité antérieure — -il n'annonçait ni cardinalité ni appartenance — donc le clone était une dette de -maintenance vivante, et pas seulement des lignes dormantes. Chaque moteur -d'intervalle conservé est un endroit où le prochain invariant transverse devra -être tissé à la main. - -Retirer des types de générateurs publics est un **changement cassant** qui touche -le socle de types supportés : c'est donc une décision produit délibérée, pas un -nettoyage de code — d'où cet ADR. - -## Décision - -Retirer `AnyInt128`, `AnyUInt128` et le moteur `WideIntervalSpec` qui n'existe -que pour eux ; et retirer `AnyHalf`. Conserver toute la matrice entière jusqu'à -64 bits et les générateurs flottants `Decimal`/`Double`/`Single` — la largeur que -l'on utilise réellement. Un test ayant besoin d'un dummy 128 bits ou `Half` -contraint utilise un littéral, ou un cast depuis le générateur immédiatement plus -large (`(Half)Any.Single().Between(...)`, un cast `Int64`). - -## Justification - -* **Le périmètre, pas l'exhaustivité.** Une bibliothèque de dummies vaut par la - trivialité des cas courants et la facilité de la majorité des cas complexes — - non par la couverture de tout type numérique du BCL à la même profondeur. - `Int128`/`UInt128`/`Half` sont la traîne où la demande de dummy contraint tend - vers zéro. -* **Un moteur de moins à tenir honnête.** Supprimer `WideIntervalSpec` retire un - moteur d'intervalle entier — un vrai concept, pas seulement des lignes — et - avec lui l'un des quatre endroits où chaque futur invariant transverse - (cardinalité, appartenance, une future capacité) devrait être ré-implémenté et - maintenu synchronisé. La dérive déjà observée en est la preuve. -* **L'échappatoire est bon marché.** La régression est bornée et locale : un cast - ou un littéral au point d'appel couvre tous les cas que servaient les - générateurs retirés, donc aucun test réaliste ne perd une capacité qu'il ne - puisse trivialement retrouver. - -## Alternatives considérées - -### Tout garder en l'état - -Le statu quo. Rejeté parce qu'il préserve un moteur entier et ~560 lignes pour un -bénéfice réel quasi nul et que — comme l'a montré la dérive silencieuse de -`WideIntervalSpec` — cette surface pourrit activement, taxant chaque futur -invariant. - -### Ne couper que les générateurs 128 bits, garder `Half` - -Défendable : `Half` repose sur le moteur continu partagé, donc il ne coûte aucun -*concept*, seulement des lignes, et notre étalon pondéré par les concepts traite -la largeur lisible sur un moteur partagé comme quasi gratuite (le même -raisonnement qui garde `Byte`…`UInt64`). C'est le repli naturel si `Half` s'avère -avoir de vrais utilisateurs ; la coupe 128 bits (qui retire le moteur) est la -partie porteuse. - -### Unifier les moteurs d'intervalle au lieu d'élaguer - -Refondre `WideIntervalSpec` dans un moteur d'intervalle générique partagé avec -`OrdinalIntervalSpec`. Rejeté : la generic math (`INumber`) est net8 -uniquement, donc sur le socle .NET Standard 2.0 il n'y a aucune abstraction -partagée *vers laquelle* unifier — les options honnêtes sont « garder le clone » -ou « retirer les types », pas « fusionner ». - -## Conséquences - -### Positives - -* L'un des quatre moteurs d'intervalle disparaît ; l'invariant - cardinalité/appartenance (et tout futur invariant) a moins de générateurs à - atteindre. -* La surface de la bibliothèque suit de plus près l'usage réel des dummies. - -### Négatives - -* Un changement cassant pour tout appelant de - `Any.Int128()`/`Any.UInt128()`/`Any.Half()` — atténué par un cast ou un - littéral au point d'appel, et signalé dans les notes de version. -* Perte d'uniformité de la matrice numérique : la liste des types ne reflète plus - un pour un les types numériques du BCL. - -### Risques - -* **Demande sous-estimée** — si les dummies 128 bits ou `Half` contraints - s'avèrent plus utilisés que prévu, la coupe est réversible (les générateurs - sont mécaniques), mais au prix du remaniement que cet ADR cherche à éviter. - Atténué en livrant le retrait dans une version majeure clairement signalée. - -## Actions de suivi - -* À l'acceptation, retirer les générateurs, `WideIntervalSpec`, leurs entrées de - fabrique `Any.*` et leurs tests ; signaler le changement cassant dans les notes - de version. -* Si seule la coupe 128 bits est acceptée (`Half` conservé), laisser `AnyHalf` - intact sur le moteur partagé. - -## Références - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) — Héberger Dummies comme un paquet autonome. -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md) — le périmètre anticipé de cardinalité dans lequel ces types ont été rentrés. -* Les générateurs et le moteur proposés au retrait, dans le projet `Dummies` - (`AnyInt128`, `AnyUInt128`, `WideIntervalSpec`, `AnyHalf`). diff --git a/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md b/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md deleted file mode 100644 index 835e5109..00000000 --- a/doc/handwritten/for-maintainers/adr/0023-prune-the-exotic-width-numeric-generators.md +++ /dev/null @@ -1,125 +0,0 @@ -# ADR-0023 | Prune the exotic-width numeric generators (128-bit and Half) - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0023-prune-the-exotic-width-numeric-generators.fr.md) - -**Status:** Proposed -**Date:** 2026-07-19 -**Decision Makers:** Reefact - -## Context - -`Dummies` ships a wide numeric matrix so that a test can ask for an arbitrary -value of almost any built-in number type: `Byte`/`SByte`, the 16/32/64-bit -integers, `Int128`/`UInt128`, `Decimal`, and `Double`/`Single`/`Half`. The goal -is producing dummies *easily* — cover every simple case and a majority of the -plausible complex ones — without becoming a constraint solver. - -Two of these types carry a cost out of proportion to their realistic use: - -* **`Int128`/`UInt128`** are backed by their own spec engine, - `WideIntervalSpec` — a ~185-line `UInt128` clone of `OrdinalIntervalSpec`, - the library's *fourth* interval engine, existing only to serve these two - generators. A constrained 128-bit dummy (a bounded range, an allow-list, an - exclusion) is a rounding-error fraction of real usage; the type is net8-only, - so it is not even a beginner's reach. -* **`Half`** rides the shared `ContinuousIntervalSpec`, so it costs no engine — - only ~207 lines of generator surface — but a `Half` dummy is a rare - ML/GPU-interop need. - -The cost became concrete while extending the eager cardinality perimeter so -that *every* finite-domain generator gates distinct collections uniformly (the -"hold the promise everywhere" work behind [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md)). -Bringing these types into the perimeter revealed that `WideIntervalSpec` had -already **silently drifted** out of the earlier cardinality capability — it -advertised neither a cardinality nor membership — so the clone was a live -maintenance liability, not merely dormant lines. Every interval engine we keep -is a place the next cross-cutting invariant must be threaded by hand. - -Removing public generator types is a **breaking change** and touches the -supported-type floor, so it is a deliberate product decision, not a code -cleanup — hence this ADR. - -## Decision - -Remove `AnyInt128`, `AnyUInt128`, and the `WideIntervalSpec` engine that exists -only for them; and remove `AnyHalf`. Keep the full integer matrix up to 64 bits -and the `Decimal`/`Double`/`Single` floating-point generators — the breadth -people actually reach for. A test needing a constrained 128-bit or `Half` dummy -uses a literal, or a cast from the next-widest generator -(`(Half)Any.Single().Between(...)`, an `Int64` cast). - -## Rationale - -* **Scope, not completeness.** A dummies library earns its keep by making the - common cases trivial and the majority of complex ones easy — not by covering - every BCL numeric type to the same depth. `Int128`/`UInt128`/`Half` are the - tail where constrained-dummy demand rounds to zero. -* **One fewer engine to keep honest.** Deleting `WideIntervalSpec` removes an - entire interval engine — a real concept, not just lines — and with it one of - the four places every future cross-cutting invariant (cardinality, membership, - a future capability) would have to be re-implemented and kept in sync. The - already-observed drift is the evidence. -* **The escape hatch is cheap.** The regression is bounded and local: a cast or - a literal at the call site covers every case the removed generators served, so - no realistic test loses a capability it cannot trivially recover. - -## Alternatives Considered - -### Keep everything as-is - -The status quo. Rejected because it preserves a whole engine and ~560 lines for -near-null realistic benefit, and — as the silent `WideIntervalSpec` drift -showed — that surface actively rots, taxing every future invariant. - -### Cut only the 128-bit generators, keep `Half` - -Tenable: `Half` rides the shared continuous engine, so it costs no *concept*, -only lines, and our concept-weighted yardstick treats legible breadth on a -shared engine as nearly free (the same reasoning that keeps `Byte`…`UInt64`). -This is the natural fallback if `Half` turns out to have real users; the 128-bit -cut (which removes the engine) is the load-bearing part. - -### Unify the interval engines instead of pruning - -Fold `WideIntervalSpec` back into a generic-math interval engine shared with -`OrdinalIntervalSpec`. Rejected: generic-math (`INumber`) is net8-only, so on -the .NET Standard 2.0 floor there is no shared abstraction to unify *into* — -the honest options are "keep the clone" or "drop the types", not "merge". - -## Consequences - -### Positive - -* One of the four interval engines disappears; the cardinality/membership - invariant (and any future one) has fewer generators to reach. -* The library's surface tracks realistic dummy usage more closely. - -### Negative - -* A breaking change for any caller using `Any.Int128()`/`Any.UInt128()`/`Any.Half()` - — mitigated by a cast or literal at the call site, and called out in the - release notes. -* Loss of uniformity in the numeric matrix: the type list no longer mirrors the - BCL's numeric types one-for-one. - -### Risks - -* **Underestimated demand** — if constrained 128-bit or `Half` dummies turn out - to be used more than expected, the cut is reversible (the generators are - mechanical), but at the cost of the churn this ADR is trying to avoid. - Mitigated by shipping the removal in a clearly-noted major version. - -## Follow-up Actions - -* On acceptance, remove the generators, `WideIntervalSpec`, their `Any.*` - factory entries, and their tests; note the breaking change in the release - notes. -* If only the 128-bit cut is accepted (Half kept), leave `AnyHalf` on the shared - engine untouched. - -## References - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) — Host Dummies as a standalone package. -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) — the eager cardinality perimeter these types were brought into. -* The generators and engine proposed for removal, in the `Dummies` project - (`AnyInt128`, `AnyUInt128`, `WideIntervalSpec`, `AnyHalf`). diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 6a33cdd0..720f2227 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -196,4 +196,3 @@ Optional supporting material: | [ADR-0020](0020-materialize-dummies-only-through-generate.md) | Materialize dummies only through Generate() | Accepted | | [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) | Bind out-of-DTO arguments as peers through a source-agnostic untyped entry | Proposed | | [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) | Floor the library's .NET Framework support at 4.7.2 | Accepted | -| [ADR-0023](0023-prune-the-exotic-width-numeric-generators.md) | Prune the exotic-width numeric generators (128-bit and Half) | Proposed |