Repository: dubzzz/fast-check
Pull request #1705 (2021-04-07 22:12:24)

♻️ Migrate integer/bigint/double to NextArbitrary
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

❌ New feature
❌ Fix an issue
❌ Documentation improvement
✔️ Other: *refactor*

(✔️: yes, ❌: no)

## Potential impacts

None expected but if any on integer/double/bigint and by transition any arbitrary offered by fast-check on:
- Generated values impact,
- Shrink values impact

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1705/builds/117366) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e90ca90ab91db815552fcbd9400bad2124833933:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-xystg)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-8ncf7)| Configuration |


Patch:
diff --git a/src/check/arbitrary/BigIntArbitrary.ts b/src/check/arbitrary/BigIntArbitrary.ts
index 0cd407c2..46fdff35 100644
--- a/src/check/arbitrary/BigIntArbitrary.ts
+++ b/src/check/arbitrary/BigIntArbitrary.ts
@@ -1,31 +1,39 @@
 import { Random } from '../../random/generator/Random';
 import { Stream } from '../../stream/Stream';
-import { Arbitrary } from './definition/Arbitrary';
 import { ArbitraryWithContextualShrink } from './definition/ArbitraryWithContextualShrink';
-import { biasWrapper } from './definition/BiasedArbitraryWrapper';
-import { Shrinkable } from './definition/Shrinkable';
-import { biasNumeric, bigIntLogLike } from './helpers/BiasNumeric';
+import { convertFromNextWithShrunkOnce } from './definition/Converters';
+import { NextArbitrary } from './definition/NextArbitrary';
+import { NextValue } from './definition/NextValue';
+import { retrieveBiasRangesForNumeric, bigIntLogLike } from './helpers/BiasNumeric';
 import { shrinkBigInt } from './helpers/ShrinkBigInt';
 
 /** @internal */
-class BigIntArbitrary extends ArbitraryWithContextualShrink<bigint> {
-  private biasedBigIntArbitrary: Arbitrary<bigint> | null = null;
-
-  constructor(readonly min: bigint, readonly max: bigint, readonly genMin: bigint, readonly genMax: bigint) {
+class BigIntArbitrary extends NextArbitrary<bigint> {
+  constructor(readonly min: bigint, readonly max: bigint) {
     super();
   }
 
-  private wrapper(value: bigint, context: unknown): Shrinkable<bigint> {
-    return new Shrinkable(value, () =>
-      this.contextualShrink(value, context).map(([v, nextContext]) => this.wrapper(v, nextContext))
-    );
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<bigint> {
+    const range = this.computeGenerateRange(mrng, biasFactor);
+    return new NextValue(mrng.nextBigInt(range.min, range.max), undefined);
+  }
+  private computeGenerateRange(mrng: Random, biasFactor: number | undefined): { min: bigint; max: bigint } {
+    if (biasFactor === undefined || mrng.nextInt(1, biasFactor) !== 1) {
+      return { min: this.min, max: this.max };
+    }
+    const ranges = retrieveBiasRangesForNumeric(this.min, this.max, bigIntLogLike);
+    if (ranges.length === 1) {
+      return ranges[0];
+    }
+    const id = mrng.nextInt(-2 * (ranges.length - 1), ranges.length - 2); // 1st range has the highest priority
+    return id < 0 ? ranges[0] : ranges[id + 1];
   }
 
-  generate(mrng: Random): Shrinkable<bigint> {
-    return this.wrapper(mrng.nextBigInt(this.genMin, this.genMax), undefined);
+  canGenerate(value: unknown): value is bigint {
+    return typeof value === 'bigint' && this.min <= value && value <= this.max;
   }
 
-  contextualShrink(current: bigint, context?: unknown): Stream<[bigint, unknown]> {
+  shrink(current: bigint, context?: unknown): Stream<NextValue<bigint>> {
     if (current === BigInt(0)) {
       return Stream.nil();
     }
@@ -40,17 +48,13 @@ class BigIntArbitrary extends ArbitraryWithContextualShrink<bigint> {
       // Last chance try...
       // context is set to undefined, so that shrink will restart
       // without any assumptions in case our try find yet another bug
-      return Stream.of([context, undefined]);
+      return Stream.of(new NextValue(context, undefined));
     }
     // Normal shrink process
     return shrinkBigInt(current, context, false);
   }
 
-  shrunkOnceContext(): unknown {
-    return this.defaultTarget();
-  }
-
-  private defaultTarget(): bigint {
+  defaultTarget(): bigint {
     // min <= 0 && max >= 0   => shrink towards zero
     if (this.min <= 0 && this.max >= 0) {
       return BigInt(0);
@@ -84,18 +88,12 @@ class BigIntArbitrary extends ArbitraryWithContextualShrink<bigint> {
     }
     return true;
   }
+}
 
-  private pureBiasedArbitrary(): Arbitrary<bigint> {
-    if (this.biasedBigIntArbitrary != null) {
-      return this.biasedBigIntArbitrary;
-    }
-    this.biasedBigIntArbitrary = biasNumeric(this.min, this.max, BigIntArbitrary, bigIntLogLike);
-    return this.biasedBigIntArbitrary;
-  }
-
-  withBias(freq: number): Arbitrary<bigint> {
-    return biasWrapper(freq, this, (originalArbitrary: BigIntArbitrary) => originalArbitrary.pureBiasedArbitrary());
-  }
+/** @internal */
+function buildBigIntArbitrary(min: bigint, max: bigint): ArbitraryWithContextualShrink<bigint> {
+  const arb = new BigIntArbitrary(min, max);
+  return convertFromNextWithShrunkOnce(arb, arb.defaultTarget());
 }
 
 /**
@@ -111,7 +109,7 @@ class BigIntArbitrary extends ArbitraryWithContextualShrink<bigint> {
 function bigIntN(n: number): ArbitraryWithContextualShrink<bigint> {
   const min = BigInt(-1) << BigInt(n - 1);
   const max = (BigInt(1) << BigInt(n - 1)) - BigInt(1);
-  return new BigIntArbitrary(min, max, min, max);
+  return buildBigIntArbitrary(min, max);
 }
 
 /**
@@ -127,7 +125,7 @@ function bigIntN(n: number): ArbitraryWithContextualShrink<bigint> {
 function bigUintN(n: number): ArbitraryWithContextualShrink<bigint> {
   const min = BigInt(0);
   const max = (BigInt(1) << BigInt(n)) - BigInt(1);
-  return new BigIntArbitrary(min, max, min, max);
+  return buildBigIntArbitrary(min, max);
 }
 
 /**
@@ -211,7 +209,7 @@ function bigInt(min: bigint, max: bigint): ArbitraryWithContextualShrink<bigint>
 function bigInt(constraints: BigIntConstraints): ArbitraryWithContextualShrink<bigint>;
 function bigInt(...args: [] | [bigint, bigint] | [BigIntConstraints]): ArbitraryWithContextualShrink<bigint> {
   const constraints = buildCompleteBigIntConstraints(extractBigIntConstraints(args));
-  return new BigIntArbitrary(constraints.min, constraints.max, constraints.min, constraints.max);
+  return buildBigIntArbitrary(constraints.min, constraints.max);
 }
 
 /**
@@ -253,7 +251,7 @@ function bigUint(max: bigint): ArbitraryWithContextualShrink<bigint>;
 function bigUint(constraints: BigUintConstraints): ArbitraryWithContextualShrink<bigint>;
 function bigUint(constraints?: bigint | BigUintConstraints): ArbitraryWithContextualShrink<bigint> {
   const max = constraints === undefined ? undefined : typeof constraints === 'object' ? constraints.max : constraints;
-  return max === undefined ? bigUintN(256) : new BigIntArbitrary(BigInt(0), max, BigInt(0), max);
+  return max === undefined ? bigUintN(256) : buildBigIntArbitrary(BigInt(0), max);
 }
 
 export { bigIntN, bigUintN, bigInt, bigUint };
diff --git a/src/check/arbitrary/IntegerArbitrary.ts b/src/check/arbitrary/IntegerArbitrary.ts
index d8c1ee3f..95e02128 100644
--- a/src/check/arbitrary/IntegerArbitrary.ts
+++ b/src/check/arbitrary/IntegerArbitrary.ts
@@ -1,34 +1,42 @@
 import { Random } from '../../random/generator/Random';
 import { Stream } from '../../stream/Stream';
-import { Arbitrary } from './definition/Arbitrary';
 import { ArbitraryWithContextualShrink } from './definition/ArbitraryWithContextualShrink';
-import { biasWrapper } from './definition/BiasedArbitraryWrapper';
-import { Shrinkable } from './definition/Shrinkable';
-import { biasNumeric, integerLogLike } from './helpers/BiasNumeric';
+import { convertFromNextWithShrunkOnce } from './definition/Converters';
+import { NextArbitrary } from './definition/NextArbitrary';
+import { NextValue } from './definition/NextValue';
+import { retrieveBiasRangesForNumeric, integerLogLike } from './helpers/BiasNumeric';
 import { shrinkInteger } from './helpers/ShrinkInteger';
 
 /** @internal */
-class IntegerArbitrary extends ArbitraryWithContextualShrink<number> {
+class IntegerArbitrary extends NextArbitrary<number> {
   static MIN_INT: number = 0x80000000 | 0;
   static MAX_INT: number = 0x7fffffff | 0;
 
-  private biasedIntegerArbitrary: Arbitrary<number> | null = null;
-
-  constructor(readonly min: number, readonly max: number, readonly genMin: number, readonly genMax: number) {
+  constructor(readonly min: number, readonly max: number) {
     super();
   }
 
-  private wrapper(value: number, context: unknown): Shrinkable<number> {
-    return new Shrinkable(value, () =>
-      this.contextualShrink(value, context).map(([v, nextContext]) => this.wrapper(v, nextContext))
-    );
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<number> {
+    const range = this.computeGenerateRange(mrng, biasFactor);
+    return new NextValue(mrng.nextInt(range.min, range.max), undefined);
+  }
+  private computeGenerateRange(mrng: Random, biasFactor: number | undefined): { min: number; max: number } {
+    if (biasFactor === undefined || mrng.nextInt(1, biasFactor) !== 1) {
+      return { min: this.min, max: this.max };
+    }
+    const ranges = retrieveBiasRangesForNumeric(this.min, this.max, integerLogLike);
+    if (ranges.length === 1) {
+      return ranges[0];
+    }
+    const id = mrng.nextInt(-2 * (ranges.length - 1), ranges.length - 2); // 1st range has the highest priority
+    return id < 0 ? ranges[0] : ranges[id + 1];
   }
 
-  generate(mrng: Random): Shrinkable<number> {
-    return this.wrapper(mrng.nextInt(this.genMin, this.genMax), undefined);
+  canGenerate(value: unknown): value is number {
+    return typeof value === 'number' && Number.isInteger(value) && this.min <= value && value <= this.max;
   }
 
-  contextualShrink(current: number, context?: unknown): Stream<[number, unknown]> {
+  shrink(current: number, context?: unknown): Stream<NextValue<number>> {
     if (current === 0) {
       return Stream.nil();
     }
@@ -43,17 +51,13 @@ class IntegerArbitrary extends ArbitraryWithContextualShrink<number> {
       // Last chance try...
       // context is set to undefined, so that shrink will restart
       // without any assumptions in case our try find yet another bug
-      return Stream.of([context, undefined]);
+      return Stream.of(new NextValue(context, undefined));
     }
     // Normal shrink process
     return shrinkInteger(current, context, false);
   }
 
-  shrunkOnceContext(): unknown {
-    return this.defaultTarget();
-  }
-
-  private defaultTarget(): number {
+  defaultTarget(): number {
     // min <= 0 && max >= 0   => shrink towards zero
     if (this.min <= 0 && this.max >= 0) {
       return 0;
@@ -89,18 +93,6 @@ class IntegerArbitrary extends ArbitraryWithContextualShrink<number> {
     }
     return true;
   }
-
-  private pureBiasedArbitrary(): Arbitrary<number> {
-    if (this.biasedIntegerArbitrary != null) {
-      return this.biasedIntegerArbitrary;
-    }
-    this.biasedIntegerArbitrary = biasNumeric<number>(this.min, this.max, IntegerArbitrary, integerLogLike);
-    return this.biasedIntegerArbitrary;
-  }
-
-  withBias(freq: number): Arbitrary<number> {
-    return biasWrapper(freq, this, (originalArbitrary: IntegerArbitrary) => originalArbitrary.pureBiasedArbitrary());
-  }
 }
 
 /**
@@ -199,7 +191,8 @@ function integer(
   if (constraints.min > constraints.max) {
     throw new Error('fc.integer maximum value should be equal or greater than the minimum one');
   }
-  return new IntegerArbitrary(constraints.min, constraints.max, constraints.min, constraints.max);
+  const arb = new IntegerArbitrary(constraints.min, constraints.max);
+  return convertFromNextWithShrunkOnce(arb, arb.defaultTarget());
 }
 
 /**
@@ -255,7 +248,8 @@ function nat(arg?: number | NatConstraints): ArbitraryWithContextualShrink<numbe
   if (max < 0) {
     throw new Error('fc.nat value should be greater than or equal to 0');
   }
-  return new IntegerArbitrary(0, max, 0, max);
+  const arb = new IntegerArbitrary(0, max);
+  return convertFromNextWithShrunkOnce(arb, arb.defaultTarget());
 }
 
 /**
diff --git a/src/check/arbitrary/helpers/ArrayInt64Arbitrary.ts b/src/check/arbitrary/helpers/ArrayInt64Arbitrary.ts
index 1c94e540..97b85ae3 100644
--- a/src/check/arbitrary/helpers/ArrayInt64Arbitrary.ts
+++ b/src/check/arbitrary/helpers/ArrayInt64Arbitrary.ts
@@ -1,9 +1,9 @@
 import { Random } from '../../../random/generator/Random';
 import { stream, Stream } from '../../../stream/Stream';
-import { Arbitrary } from '../definition/Arbitrary';
 import { ArbitraryWithContextualShrink } from '../definition/ArbitraryWithContextualShrink';
-import { biasWrapper } from '../definition/BiasedArbitraryWrapper';
-import { Shrinkable } from '../definition/Shrinkable';
+import { convertFromNextWithShrunkOnce } from '../definition/Converters';
+import { NextArbitrary } from '../definition/NextArbitrary';
+import { NextValue } from '../definition/NextValue';
 import {
   add64,
   ArrayInt64,
@@ -11,61 +11,76 @@ import {
   isEqual64,
   isStrictlyNegative64,
   isStrictlyPositive64,
+  isStrictlySmaller64,
   isZero64,
   logLike64,
   substract64,
   Unit64,
   Zero64,
 } from './ArrayInt64';
-import { BiasedNumericArbitrary } from './BiasNumeric';
 
 /** @internal */
-class ArrayInt64Arbitrary extends ArbitraryWithContextualShrink<ArrayInt64> {
-  private biasedArrayInt64Arbitrary: Arbitrary<ArrayInt64> | null = null;
+class ArrayInt64Arbitrary extends NextArbitrary<ArrayInt64> {
+  private biasedRanges: { min: ArrayInt64; max: ArrayInt64 }[] | null = null;
 
-  constructor(
-    readonly min: ArrayInt64,
-    readonly max: ArrayInt64,
-    readonly genMin: ArrayInt64,
-    readonly genMax: ArrayInt64
-  ) {
+  constructor(readonly min: ArrayInt64, readonly max: ArrayInt64) {
     super();
   }
 
-  private wrapper(value: ArrayInt64, context: unknown): Shrinkable<ArrayInt64> {
-    return new Shrinkable(value, () =>
-      this.contextualShrink(value, context).map(([v, nextContext]) => this.wrapper(v, nextContext))
-    );
-  }
-
-  generate(mrng: Random): Shrinkable<ArrayInt64> {
-    const uncheckedValue = mrng.nextArrayInt(this.genMin, this.genMax);
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<ArrayInt64> {
+    const range = this.computeGenerateRange(mrng, biasFactor);
+    const uncheckedValue = mrng.nextArrayInt(range.min, range.max);
     if (uncheckedValue.data.length === 1) {
       // either 1 or 2, never 0 or >2
       uncheckedValue.data.unshift(0); // prepend a zero
     }
-    return this.wrapper(uncheckedValue as ArrayInt64, undefined);
+    return new NextValue(uncheckedValue as ArrayInt64, undefined);
+  }
+  private computeGenerateRange(mrng: Random, biasFactor: number | undefined): { min: ArrayInt64; max: ArrayInt64 } {
+    if (biasFactor === undefined || mrng.nextInt(1, biasFactor) !== 1) {
+      return { min: this.min, max: this.max };
+    }
+    const ranges = this.retrieveBiasedRanges();
+    if (ranges.length === 1) {
+      return ranges[0];
+    }
+    const id = mrng.nextInt(-2 * (ranges.length - 1), ranges.length - 2); // 1st range has the highest priority
+    return id < 0 ? ranges[0] : ranges[id + 1];
+  }
+
+  canGenerate(value: unknown): value is ArrayInt64 {
+    const unsafeValue = value as ArrayInt64;
+    return (
+      typeof value === 'object' &&
+      value !== null &&
+      (unsafeValue.sign === -1 || unsafeValue.sign === 1) &&
+      Array.isArray(unsafeValue.data) &&
+      unsafeValue.data.length === 2 &&
+      ((isStrictlySmaller64(this.min, unsafeValue) && isStrictlySmaller64(unsafeValue, this.max)) ||
+        isEqual64(this.min, unsafeValue) ||
+        isEqual64(this.max, unsafeValue))
+    );
   }
 
   private shrinkArrayInt64(
     value: ArrayInt64,
     target: ArrayInt64,
     tryTargetAsap?: boolean
-  ): Stream<[ArrayInt64, unknown]> {
+  ): Stream<NextValue<ArrayInt64>> {
     const realGap = substract64(value, target);
-    function* shrinkGen(): IterableIterator<[ArrayInt64, unknown]> {
+    function* shrinkGen(): IterableIterator<NextValue<ArrayInt64>> {
       let previous: ArrayInt64 | undefined = tryTargetAsap ? undefined : target;
       const gap = tryTargetAsap ? realGap : halve64(realGap);
       for (let toremove = gap; !isZero64(toremove); toremove = halve64(toremove)) {
         const next = substract64(value, toremove);
-        yield [next, previous]; // previous indicates the last passing value
+        yield new NextValue(next, previous); // previous indicates the last passing value
         previous = next;
       }
     }
     return stream(shrinkGen());
   }
 
-  contextualShrink(current: ArrayInt64, context?: unknown): Stream<[ArrayInt64, unknown]> {
+  shrink(current: ArrayInt64, context?: unknown): Stream<NextValue<ArrayInt64>> {
     if (!ArrayInt64Arbitrary.isValidContext(current, context)) {
       // No context:
       //   Take default target and shrink towards it
@@ -77,17 +92,13 @@ class ArrayInt64Arbitrary extends ArbitraryWithContextualShrink<ArrayInt64> {
       // Last chance try...
       // context is set to undefined, so that shrink will restart
       // without any assumptions in case our try find yet another bug
-      return Stream.of([context, undefined]);
+      return Stream.of(new NextValue(context, undefined));
     }
     // Normal shrink process
     return this.shrinkArrayInt64(current, context, false);
   }
 
-  shrunkOnceContext(): unknown {
-    return this.defaultTarget();
-  }
-
-  private defaultTarget(): ArrayInt64 {
+  defaultTarget(): ArrayInt64 {
     // min <= 0 && max >= 0   => shrink towards zero
     if (!isStrictlyPositive64(this.min) && !isStrictlyNegative64(this.max)) {
       return Zero64;
@@ -123,13 +134,13 @@ class ArrayInt64Arbitrary extends ArbitraryWithContextualShrink<ArrayInt64> {
     return true;
   }
 
-  private pureBiasedArbitrary(): Arbitrary<ArrayInt64> {
-    if (this.biasedArrayInt64Arbitrary != null) {
-      return this.biasedArrayInt64Arbitrary;
+  private retrieveBiasedRanges(): { min: ArrayInt64; max: ArrayInt64 }[] {
+    if (this.biasedRanges != null) {
+      return this.biasedRanges;
     }
     if (isEqual64(this.min, this.max)) {
-      this.biasedArrayInt64Arbitrary = this;
-      return this;
+      this.biasedRanges = [{ min: this.min, max: this.max }];
+      return this.biasedRanges;
     }
     const minStrictlySmallerZero = isStrictlyNegative64(this.min);
     const maxStrictlyGreaterZero = isStrictlyPositive64(this.max);
@@ -137,30 +148,27 @@ class ArrayInt64Arbitrary extends ArbitraryWithContextualShrink<ArrayInt64> {
       // min < 0 && max > 0
       const logMin = logLike64(this.min); // min !== 0   ->   <=0
       const logMax = logLike64(this.max); // max !== 0   ->   >=0
-      this.biasedArrayInt64Arbitrary = new BiasedNumericArbitrary(
-        new ArrayInt64Arbitrary(this.min, this.max, logMin, logMax), // close to zero,
-        new ArrayInt64Arbitrary(this.min, this.max, substract64(this.max, logMax), this.max), // close to max
-        new ArrayInt64Arbitrary(this.min, this.max, this.min, substract64(this.min, logMin)) // close to min
-      );
+      this.biasedRanges = [
+        { min: logMin, max: logMax }, // close to zero,
+        { min: substract64(this.max, logMax), max: this.max }, // close to max
+        { min: this.min, max: substract64(this.min, logMin) }, // close to min
+      ];
     } else {
       // Either min < 0 && max <= 0
       // Or min >= 0, so max >= 0
       const logGap = logLike64(substract64(this.max, this.min)); // max-min !== 0  ->  >=0
-      const arbCloseToMin = new ArrayInt64Arbitrary(this.min, this.max, this.min, add64(this.min, logGap)); // close to min
-      const arbCloseToMax = new ArrayInt64Arbitrary(this.min, this.max, substract64(this.max, logGap), this.max); // close to max
-      this.biasedArrayInt64Arbitrary = minStrictlySmallerZero
-        ? new BiasedNumericArbitrary(arbCloseToMax, arbCloseToMin) // max is closer to zero
-        : new BiasedNumericArbitrary(arbCloseToMin, arbCloseToMax); // min is closer to zero
+      const arbCloseToMin = { min: this.min, max: add64(this.min, logGap) }; // close to min
+      const arbCloseToMax = { min: substract64(this.max, logGap), max: this.max }; // close to max
+      this.biasedRanges = minStrictlySmallerZero
+        ? [arbCloseToMax, arbCloseToMin] // max is closer to zero
+        : [arbCloseToMin, arbCloseToMax]; // min is closer to zero
     }
-    return this.biasedArrayInt64Arbitrary;
-  }
-
-  withBias(freq: number): Arbitrary<ArrayInt64> {
-    return biasWrapper(freq, this, (originalArbitrary: ArrayInt64Arbitrary) => originalArbitrary.pureBiasedArbitrary());
+    return this.biasedRanges;
   }
 }
 
 /** @internal */
 export function arrayInt64(min: ArrayInt64, max: ArrayInt64): ArbitraryWithContextualShrink<ArrayInt64> {
-  return new ArrayInt64Arbitrary(min, max, min, max);
+  const arb = new ArrayInt64Arbitrary(min, max);
+  return convertFromNextWithShrunkOnce(arb, arb.defaultTarget());
 }
diff --git a/src/check/arbitrary/helpers/BiasNumeric.ts b/src/check/arbitrary/helpers/BiasNumeric.ts
index 1e5ffb94..2ead4d83 100644
--- a/src/check/arbitrary/helpers/BiasNumeric.ts
+++ b/src/check/arbitrary/helpers/BiasNumeric.ts
@@ -1,56 +1,44 @@
-import { Random } from '../../../random/generator/Random';
-import { Arbitrary } from '../definition/Arbitrary';
-import { Shrinkable } from '../definition/Shrinkable';
-
-/** @internal */
-type Numeric = number | bigint;
-
 /** @internal */
-type NumericArbitrary<NType> = new (min: NType, max: NType, genMin: NType, genMax: NType) => Arbitrary<NType>;
-
-/** @internal */
-export class BiasedNumericArbitrary<NType> extends Arbitrary<NType> {
-  private readonly arbs: Arbitrary<NType>[];
-  constructor(readonly arbCloseToZero: Arbitrary<NType>, ...arbs: Arbitrary<NType>[]) {
-    super();
-    this.arbs = arbs;
-  }
-  generate(mrng: Random): Shrinkable<NType> {
-    const id = mrng.nextInt(-2 * this.arbs.length, this.arbs.length - 1); // 2 close to zero for 1 in others
-    return id < 0 ? this.arbCloseToZero.generate(mrng) : this.arbs[id].generate(mrng);
-  }
-}
-
-/** @internal */
-export function biasNumeric<NType extends Numeric>(
+function retrieveBiasRangesForNumeric(
+  min: number,
+  max: number,
+  logLike: (n: number) => number
+): { min: number; max: number }[];
+function retrieveBiasRangesForNumeric(
+  min: bigint,
+  max: bigint,
+  logLike: (n: bigint) => bigint
+): { min: bigint; max: bigint }[];
+function retrieveBiasRangesForNumeric<NType extends number | bigint>(
   min: NType,
   max: NType,
-  Ctor: NumericArbitrary<NType>,
   logLike: (n: NType) => NType
-): Arbitrary<NType> {
+): { min: NType; max: NType }[] {
   if (min === max) {
-    return new Ctor(min, max, min, max);
+    return [{ min: min, max: max }];
   }
   if (min < 0 && max > 0) {
     // min < 0 && max > 0
     const logMin = logLike(-min as any); // min !== 0
     const logMax = logLike(max); // max !== 0
-    return new BiasedNumericArbitrary(
-      new Ctor(min, max, -logMin as any, logMax), // close to zero,
-      new Ctor(min, max, (max - logMax) as any, max), // close to max
-      new Ctor(min, max, min, (min as any) + logMin) // close to min
-    );
+    return [
+      { min: -logMin as any, max: logMax }, // close to zero,
+      { min: (max - logMax) as any, max: max }, // close to max
+      { min: min, max: (min as any) + logMin }, // close to min
+    ];
   }
   // Either min < 0 && max <= 0
   // Or min >= 0, so max >= 0
   const logGap = logLike((max - min) as any); // max-min !== 0
-  const arbCloseToMin = new Ctor(min, max, min, (min as any) + logGap); // close to min
-  const arbCloseToMax = new Ctor(min, max, (max - logGap) as any, max); // close to max
+  const arbCloseToMin = { min: min, max: (min as any) + logGap }; // close to min
+  const arbCloseToMax = { min: (max - logGap) as any, max: max }; // close to max
   return min < 0
-    ? new BiasedNumericArbitrary(arbCloseToMax, arbCloseToMin) // max is closer to zero
-    : new BiasedNumericArbitrary(arbCloseToMin, arbCloseToMax); // min is closer to zero
+    ? [arbCloseToMax, arbCloseToMin] // max is closer to zero
+    : [arbCloseToMin, arbCloseToMax]; // min is closer to zero
 }
 
+export { retrieveBiasRangesForNumeric };
+
 /** @internal */
 export function integerLogLike(v: number): number {
   return Math.floor(Math.log(v) / Math.log(2));
diff --git a/src/check/arbitrary/helpers/ShrinkBigInt.ts b/src/check/arbitrary/helpers/ShrinkBigInt.ts
index 623e1e20..9302f742 100644
--- a/src/check/arbitrary/helpers/ShrinkBigInt.ts
+++ b/src/check/arbitrary/helpers/ShrinkBigInt.ts
@@ -1,4 +1,5 @@
 import { Stream, stream } from '../../../stream/Stream';
+import { NextValue } from '../definition/NextValue';
 
 /**
  * Halve towards zero
@@ -12,23 +13,23 @@ function halveBigInt(n: bigint): bigint {
  * Compute shrunk values to move from current to target
  * @internal
  */
-export function shrinkBigInt(current: bigint, target: bigint, tryTargetAsap: boolean): Stream<[bigint, unknown]> {
+export function shrinkBigInt(current: bigint, target: bigint, tryTargetAsap: boolean): Stream<NextValue<bigint>> {
   const realGap = current - target;
-  function* shrinkDecr(): IterableIterator<[bigint, unknown]> {
+  function* shrinkDecr(): IterableIterator<NextValue<bigint>> {
     let previous: bigint | undefined = tryTargetAsap ? undefined : target;
     const gap = tryTargetAsap ? realGap : halveBigInt(realGap);
     for (let toremove = gap; toremove > 0; toremove = halveBigInt(toremove)) {
       const next = current - toremove;
-      yield [next, previous]; // previous indicates the last passing value
+      yield new NextValue(next, previous); // previous indicates the last passing value
       previous = next;
     }
   }
-  function* shrinkIncr(): IterableIterator<[bigint, unknown]> {
+  function* shrinkIncr(): IterableIterator<NextValue<bigint>> {
     let previous: bigint | undefined = tryTargetAsap ? undefined : target;
     const gap = tryTargetAsap ? realGap : halveBigInt(realGap);
     for (let toremove = gap; toremove < 0; toremove = halveBigInt(toremove)) {
       const next = current - toremove;
-      yield [next, previous]; // previous indicates the last passing value
+      yield new NextValue(next, previous); // previous indicates the last passing value
       previous = next;
     }
   }
diff --git a/src/check/arbitrary/helpers/ShrinkInteger.ts b/src/check/arbitrary/helpers/ShrinkInteger.ts
index 45997007..d96788bd 100644
--- a/src/check/arbitrary/helpers/ShrinkInteger.ts
+++ b/src/check/arbitrary/helpers/ShrinkInteger.ts
@@ -1,4 +1,5 @@
 import { Stream, stream } from '../../../stream/Stream';
+import { NextValue } from '../definition/NextValue';
 
 /** @internal */
 function halvePosInteger(n: number): number {
@@ -13,23 +14,23 @@ function halveNegInteger(n: number): number {
  * Compute shrunk values to move from current to target
  * @internal
  */
-export function shrinkInteger(current: number, target: number, tryTargetAsap: boolean): Stream<[number, unknown]> {
+export function shrinkInteger(current: number, target: number, tryTargetAsap: boolean): Stream<NextValue<number>> {
   const realGap = current - target;
-  function* shrinkDecr(): IterableIterator<[number, unknown]> {
+  function* shrinkDecr(): IterableIterator<NextValue<number>> {
     let previous: number | undefined = tryTargetAsap ? undefined : target;
     const gap = tryTargetAsap ? realGap : halvePosInteger(realGap);
     for (let toremove = gap; toremove > 0; toremove = halvePosInteger(toremove)) {
       const next = current - toremove;
-      yield [next, previous]; // previous indicates the last passing value
+      yield new NextValue(next, previous); // previous indicates the last passing value
       previous = next;
     }
   }
-  function* shrinkIncr(): IterableIterator<[number, unknown]> {
+  function* shrinkIncr(): IterableIterator<NextValue<number>> {
     let previous: number | undefined = tryTargetAsap ? undefined : target;
     const gap = tryTargetAsap ? realGap : halveNegInteger(realGap);
     for (let toremove = gap; toremove < 0; toremove = halveNegInteger(toremove)) {
       const next = current - toremove;
-      yield [next, previous]; // previous indicates the last passing value
+      yield new NextValue(next, previous); // previous indicates the last passing value
       previous = next;
     }
   }


Test patch:
diff --git a/test/unit/check/arbitrary/generic/RandomHelpers.ts b/test/unit/check/arbitrary/generic/RandomHelpers.ts
new file mode 100644
index 00000000..7d8c365e
--- /dev/null
+++ b/test/unit/check/arbitrary/generic/RandomHelpers.ts
@@ -0,0 +1,32 @@
+import { MaybeMocked } from 'ts-jest/dist/utils/testing';
+import { Random } from '../../../../../src/random/generator/Random';
+
+export function fakeRandom(): { instance: Random } & Omit<MaybeMocked<Random>, 'internalRng' | 'uniformIn'> {
+  const clone = jest.fn();
+  const next = jest.fn();
+  const nextBoolean = jest.fn();
+  const nextInt = jest.fn();
+  const nextBigInt = jest.fn();
+  const nextArrayInt = jest.fn();
+  const nextDouble = jest.fn();
+  class MyRandom extends Random {
+    clone = clone;
+    next = next;
+    nextBoolean = nextBoolean;
+    nextInt = nextInt;
+    nextBigInt = nextBigInt;
+    nextArrayInt = nextArrayInt;
+    nextDouble = nextDouble;
+  }
+
+  return {
+    instance: new MyRandom(null as any),
+    clone,
+    next,
+    nextBoolean,
+    nextInt,
+    nextBigInt,
+    nextArrayInt,
+    nextDouble,
+  };
+}
diff --git a/test/unit/check/arbitrary/helpers/ArrayInt64Arbitrary.utest.spec.ts b/test/unit/check/arbitrary/helpers/ArrayInt64Arbitrary.utest.spec.ts
index 1c08e48d..41779b22 100644
--- a/test/unit/check/arbitrary/helpers/ArrayInt64Arbitrary.utest.spec.ts
+++ b/test/unit/check/arbitrary/helpers/ArrayInt64Arbitrary.utest.spec.ts
@@ -1,21 +1,22 @@
 import * as fc from '../../../../../lib/fast-check';
 
+import { arrayInt64 as arrayInt64Old } from '../../../../../src/check/arbitrary/helpers/ArrayInt64Arbitrary';
 import { ArrayInt64 } from '../../../../../src/check/arbitrary/helpers/ArrayInt64';
-import { arrayInt64 } from '../../../../../src/check/arbitrary/helpers/ArrayInt64Arbitrary';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { convertToNext } from '../../../../../src/check/arbitrary/definition/Converters';
 
-import { mocked } from 'ts-jest/utils';
-import { Random } from '../../../../../src/random/generator/Random';
-import { buildShrinkTree, renderTree } from '../generic/ShrinkTree';
+import { buildNextShrinkTree, buildShrinkTree, renderTree } from '../generic/ShrinkTree';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { fakeRandom } from '../generic/RandomHelpers';
 
-import * as BiasedArbitraryWrapperMock from '../../../../../src/check/arbitrary/definition/BiasedArbitraryWrapper';
-import * as BiasNumericMock from '../../../../../src/check/arbitrary/helpers/BiasNumeric';
-jest.mock('../../../../../src/check/arbitrary/definition/BiasedArbitraryWrapper');
-jest.mock('../../../../../src/check/arbitrary/helpers/BiasNumeric');
+function arrayInt64(...args: Parameters<typeof arrayInt64Old>): NextArbitrary<ArrayInt64> {
+  return convertToNext(arrayInt64Old(...args));
+}
 
-function toArrayInt64(b: bigint): ArrayInt64 {
+function toArrayInt64(b: bigint, withNegativeZero: boolean): ArrayInt64 {
   const posB = b < BigInt(0) ? -b : b;
   return {
-    sign: b < BigInt(0) ? -1 : 1,
+    sign: b < BigInt(0) || (withNegativeZero && b === BigInt(0)) ? -1 : 1,
     data: [Number(posB >> BigInt(32)), Number(posB & ((BigInt(1) << BigInt(32)) - BigInt(1)))],
   };
 }
@@ -24,20 +25,7 @@ function toBigInt(a: ArrayInt64): bigint {
   return BigInt(a.sign) * ((BigInt(a.data[0]) << BigInt(32)) + BigInt(a.data[1]));
 }
 
-beforeEach(() => {
-  jest.clearAllMocks();
-  jest.resetAllMocks();
-});
-const previousGlobal = fc.readConfigureGlobal();
-fc.configureGlobal({
-  ...previousGlobal,
-  beforeEach: () => {
-    jest.clearAllMocks();
-    jest.resetAllMocks();
-  },
-});
-
-describe('ArrayInt64', () => {
+describe('arrayInt64', () => {
   if (typeof BigInt === 'undefined') {
     it('no test', () => {
       expect(true).toBe(true);
@@ -45,108 +33,173 @@ describe('ArrayInt64', () => {
     return;
   }
 
-  const MaxArrayIntValue = (BigInt(1) << BigInt(64)) - BigInt(1);
-
-  const constraintsArb = () =>
-    fc
-      .tuple(
-        fc.bigInt({ min: -MaxArrayIntValue, max: MaxArrayIntValue }),
-        fc.bigInt({ min: -MaxArrayIntValue, max: MaxArrayIntValue })
-      )
-      .map((vs) => ({
-        min: vs[0] <= vs[1] ? vs[0] : vs[1],
-        max: vs[0] <= vs[1] ? vs[1] : vs[0],
-      }));
-
-  describe('arrayInt64', () => {
-    describe('withBias', () => {
-      it('Should preserve biased instance across calls', () =>
-        fc.assert(
-          fc.property(constraintsArb(), fc.integer({ min: 2 }), fc.integer({ min: 2 }), (ct, freq1, freq2) => {
+  const MinArrayIntValue = -(BigInt(2) ** BigInt(64)) + BigInt(1);
+  const MaxArrayIntValue = BigInt(2) ** BigInt(64) - BigInt(1);
+
+  describe('generate', () => {
+    it('should always consider the full range when not biased', () =>
+      fc.assert(
+        fc.property(
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.boolean(),
+          fc.boolean(),
+          (a, b, c, negMin, negMax) => {
             // Arrange
-            const { biasWrapper } = mocked(BiasedArbitraryWrapperMock);
-            const rawArbitrary = arrayInt64(toArrayInt64(ct.min), toArrayInt64(ct.max));
+            const [min, mid, max] = [a, b, c].sort((a, b) => Number(a - b));
+            const min64 = toArrayInt64(min, negMin);
+            const mid64 = toArrayInt64(mid, false);
+            const max64 = toArrayInt64(max, negMax);
+            const { instance: mrng, nextArrayInt, nextInt } = fakeRandom();
+            nextArrayInt.mockReturnValueOnce(mid64);
 
             // Act
-            rawArbitrary.withBias(freq1);
-            rawArbitrary.withBias(freq2);
+            const arb = arrayInt64(min64, max64);
+            const out = arb.generate(mrng, undefined);
 
             // Assert
-            expect(biasWrapper).toHaveBeenCalledTimes(2); // called each time for the moment
-            expect(biasWrapper).toHaveBeenCalledWith(freq1, rawArbitrary, expect.any(Function));
-            expect(biasWrapper).toHaveBeenCalledWith(freq2, rawArbitrary, expect.any(Function));
-            const [[, , biasedBuilder1], [, , biasedBuilder2]] = biasWrapper.mock.calls;
-            expect(biasedBuilder2(rawArbitrary)).toBe(biasedBuilder1(rawArbitrary));
-          })
-        ));
-
-      it('Should build biased instances compatible with initial constraints', () =>
-        fc.assert(
-          fc.property(constraintsArb(), fc.integer({ min: 2 }), (ct, freq) => {
+            expect(out.value).toBe(mid64);
+            expect(nextArrayInt).toHaveBeenCalledTimes(1);
+            expect(nextArrayInt).toHaveBeenCalledWith(min64, max64);
+            expect(nextInt).not.toHaveBeenCalled();
+          }
+        )
+      ));
+
+    it('should always consider the full range when bias should not apply', () =>
+      fc.assert(
+        fc.property(
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.boolean(),
+          fc.boolean(),
+          fc.integer({ min: 2 }),
+          (a, b, c, negMin, negMax, biasFactor) => {
             // Arrange
-            fc.pre(ct.min !== ct.max); // Otherwise we have a special case (biased version is itself)
-            const { biasWrapper } = mocked(BiasedArbitraryWrapperMock);
-            const { BiasedNumericArbitrary } = mocked(BiasNumericMock);
-            const rawArbitrary = arrayInt64(toArrayInt64(ct.min), toArrayInt64(ct.max));
+            const [min, mid, max] = [a, b, c].sort((a, b) => Number(a - b));
+            const min64 = toArrayInt64(min, negMin);
+            const mid64 = toArrayInt64(mid, false);
+            const max64 = toArrayInt64(max, negMax);
+            const { instance: mrng, nextArrayInt, nextInt } = fakeRandom();
+            nextArrayInt.mockReturnValueOnce(mid64);
+            nextInt.mockImplementationOnce((low, _high) => low + 1); // >low is no bias case
 
             // Act
-            rawArbitrary.withBias(freq);
-            const [, , biasedBuilder] = biasWrapper.mock.calls[0];
-            expect(BiasedNumericArbitrary).not.toHaveBeenCalled();
-            biasedBuilder(rawArbitrary); // Triggers calls to BiasedNumericArbitrary
+            const arb = arrayInt64(min64, max64);
+            const out = arb.generate(mrng, biasFactor);
 
             // Assert
-            expect(BiasedNumericArbitrary).toHaveBeenCalledTimes(1);
-            const biasedArbs = (BiasedNumericArbitrary as any).mock.calls[0];
-            expect(Array.isArray(biasedArbs)).toBe(true);
-            expect(biasedArbs).not.toHaveLength(0);
-            for (const biasedArb of biasedArbs) {
-              expect(biasedArb.constructor).toBe(rawArbitrary.constructor);
-              const nextArrayInt = jest.fn().mockImplementation((min) => min);
-              biasedArb.generate(({ nextArrayInt } as any) as Random);
-              expect(nextArrayInt).toHaveBeenCalledTimes(1);
-              const [min64, max64] = nextArrayInt.mock.calls[0];
-              const [min, max] = [toBigInt(min64), toBigInt(max64)];
-              expect(min).toBeLessThanOrEqual(max);
-              expect(min).toBeGreaterThanOrEqual(ct.min);
-              expect(min).toBeLessThanOrEqual(ct.max);
-              expect(max).toBeGreaterThanOrEqual(ct.min);
-              expect(max).toBeLessThanOrEqual(ct.max);
-            }
-          })
-        ));
-
-      it('Should bias towards itself for single value ranges', () =>
-        fc.assert(
-          fc.property(
-            fc.bigInt({ min: -MaxArrayIntValue, max: MaxArrayIntValue }),
-            fc.integer({ min: 2 }),
-            (minMax, freq) => {
-              // Arrange
-              const { biasWrapper } = mocked(BiasedArbitraryWrapperMock);
-              const rawArbitrary = arrayInt64(toArrayInt64(minMax), toArrayInt64(minMax));
-
-              // Act
-              rawArbitrary.withBias(freq);
-
-              // Assert
-              expect(biasWrapper).toHaveBeenCalledTimes(1);
-              expect(biasWrapper).toHaveBeenCalledWith(freq, rawArbitrary, expect.any(Function));
-              const [, , biasedBuilder] = biasWrapper.mock.calls[0];
-              expect(biasedBuilder(rawArbitrary)).toBe(rawArbitrary);
-            }
-          )
-        ));
-    });
+            expect(out.value).toBe(mid64);
+            expect(nextArrayInt).toHaveBeenCalledTimes(1);
+            expect(nextArrayInt).toHaveBeenCalledWith(min64, max64);
+            expect(nextInt).toHaveBeenCalledTimes(1);
+            expect(nextInt).toHaveBeenCalledWith(1, biasFactor);
+          }
+        )
+      ));
+
+    it('should consider sub-ranges when bias applies', () =>
+      fc.assert(
+        fc.property(
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.boolean(),
+          fc.boolean(),
+          fc.integer({ min: 2 }),
+          fc.nat(),
+          (a, b, negMin, negMax, biasFactor, r) => {
+            // Arrange
+            const [min, max] = a < b ? [a, b] : [b, a];
+            fc.pre(max - min >= BigInt(100)); // large enough range (arbitrary value)
+            const min64 = toArrayInt64(min, negMin);
+            const max64 = toArrayInt64(max, negMax);
+            const { instance: mrng, nextArrayInt, nextInt } = fakeRandom();
+            nextArrayInt.mockImplementationOnce((low, _high) => low);
+            nextInt
+              .mockImplementationOnce((low, _high) => low) // low is bias case for first call
+              .mockImplementationOnce((low, high) => low + (r % (high - low + 1))); // random inside the provided range (bias selection step)
+
+            // Act
+            const arb = arrayInt64(min64, max64);
+            arb.generate(mrng, biasFactor);
+
+            // Assert
+            expect(nextInt).toHaveBeenCalledTimes(2);
+            expect(nextArrayInt).toHaveBeenCalledTimes(1);
+            expect(nextArrayInt).not.toHaveBeenCalledWith(min64, max64);
+            const receivedMin = toBigInt(nextArrayInt.mock.calls[0][0] as ArrayInt64);
+            const receivedMax = toBigInt(nextArrayInt.mock.calls[0][1] as ArrayInt64);
+            expect(receivedMin).toBeGreaterThanOrEqual(min);
+            expect(receivedMin).toBeLessThanOrEqual(max);
+            expect(receivedMax).toBeGreaterThanOrEqual(min);
+            expect(receivedMax).toBeLessThanOrEqual(max);
+          }
+        )
+      ));
+  });
+
+  describe('canGenerate', () => {
+    it('should recognize any value it could have generated', () =>
+      fc.assert(
+        fc.property(
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.boolean(),
+          fc.boolean(),
+          fc.boolean(),
+          (a, b, c, negMin, negMid, negMax) => {
+            // Arrange
+            const [min, mid, max] = [a, b, c].sort((a, b) => Number(a - b));
+
+            // Act
+            const arb = arrayInt64(toArrayInt64(min, negMin), toArrayInt64(max, negMax));
+            const out = arb.canGenerate(toArrayInt64(mid, negMid));
+
+            // Assert
+            expect(out).toBe(true);
+          }
+        )
+      ));
+
+    it('should reject values outside of its range', () =>
+      fc.assert(
+        fc.property(
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.bigInt(MinArrayIntValue, MaxArrayIntValue),
+          fc.boolean(),
+          fc.boolean(),
+          fc.boolean(),
+          fc.constantFrom(...(['lower', 'higher'] as const)),
+          (a, b, c, negMin, negSelected, negMax, type) => {
+            // Arrange
+            const sorted = [a, b, c].sort((a, b) => Number(a - b));
+            const [min, max, selected] =
+              type === 'lower' ? [sorted[1], sorted[2], sorted[0]] : [sorted[0], sorted[1], sorted[2]];
+            fc.pre(selected < min || selected > max);
+
+            // Act
+            const arb = arrayInt64(toArrayInt64(min, negMin), toArrayInt64(max, negMax));
+            const out = arb.canGenerate(toArrayInt64(selected, negSelected));
+
+            // Assert
+            expect(out).toBe(false);
+          }
+        )
+      ));
   });
 
-  describe('contextualShrinkableFor', () => {
-    it('Should shrink strictly positive value for positive range including zero', () => {
+  describe('shrink', () => {
+    it('should shrink strictly positive value for positive range including zero', () => {
       // Arrange
       const arb = arrayInt64({ sign: 1, data: [0, 0] }, { sign: 1, data: [0, 10] });
+      const source = new NextValue({ sign: 1, data: [0, 8] }); // no context
 
       // Act
-      const tree = buildShrinkTree(arb.contextualShrinkableFor({ sign: 1, data: [0, 8] }));
+      const tree = buildNextShrinkTree(arb, source);
       const renderedTree = renderTree(tree).join('\n');
 
       // Assert
@@ -193,12 +246,13 @@ describe('ArrayInt64', () => {
                              └> {\\"sign\\":1,\\"data\\":[0,0]}"
       `);
     });
-    it('Should shrink strictly positive value for range not including zero', () => {
+    it('should shrink strictly positive value for range not including zero', () => {
       // Arrange
       const arb = arrayInt64({ sign: 1, data: [1, 10] }, { sign: 1, data: [1, 20] });
+      const source = new NextValue({ sign: 1, data: [1, 18] }); // no context
 
       // Act
-      const tree = buildShrinkTree(arb.contextualShrinkableFor({ sign: 1, data: [1, 18] }));
+      const tree = buildNextShrinkTree(arb, source);
       const renderedTree = renderTree(tree).join('\n');
 
       // Assert
@@ -245,12 +299,13 @@ describe('ArrayInt64', () => {
                              └> {\\"sign\\":1,\\"data\\":[1,10]}"
       `);
     });
-    it('Should shrink strictly negative value for negative range including zero', () => {
+    it('should shrink strictly negative value for negative range including zero', () => {
       // Arrange
       const arb = arrayInt64({ sign: -1, data: [0, 10] }, { sign: 1, data: [0, 0] });
+      const source = new NextValue({ sign: -1, data: [0, 8] }); // no context
 
       // Act
-      const tree = buildShrinkTree(arb.contextualShrinkableFor({ sign: -1, data: [0, 8] }));
+      const tree = buildNextShrinkTree(arb, source);
       const renderedTree = renderTree(tree).join('\n');
 
       // Assert
@@ -298,4 +353,46 @@ describe('ArrayInt64', () => {
       `);
     });
   });
+
+  describe('contextualShrinkableFor (old)', () => {
+    it('should shrink, as context-less, strictly positive value for positive range including zero', () => {
+      // Arrange
+      const arbNew = arrayInt64({ sign: 1, data: [0, 0] }, { sign: 1, data: [0, 10] });
+      const arbOld = arrayInt64Old({ sign: 1, data: [0, 0] }, { sign: 1, data: [0, 10] });
+      const sourceValue: ArrayInt64 = { sign: 1, data: [0, 8] };
+
+      // Act
+      const treeNew = buildNextShrinkTree(arbNew, new NextValue(sourceValue));
+      const treeOld = buildShrinkTree(arbOld.contextualShrinkableFor(sourceValue));
+
+      // Assert
+      expect(treeOld).toEqual(treeNew);
+    });
+    it('should shrink, as context-less, strictly positive value for range not including zero', () => {
+      // Arrange
+      const arbNew = arrayInt64({ sign: 1, data: [1, 10] }, { sign: 1, data: [1, 20] });
+      const arbOld = arrayInt64Old({ sign: 1, data: [1, 10] }, { sign: 1, data: [1, 20] });
+      const sourceValue: ArrayInt64 = { sign: 1, data: [1, 18] };
+
+      // Act
+      const treeNew = buildNextShrinkTree(arbNew, new NextValue(sourceValue));
+      const treeOld = buildShrinkTree(arbOld.contextualShrinkableFor(sourceValue));
+
+      // Assert
+      expect(treeOld).toEqual(treeNew);
+    });
+    it('should shrink, as context-less, strictly negative value for negative range including zero', () => {
+      // Arrange
+      const arbNew = arrayInt64({ sign: -1, data: [0, 10] }, { sign: 1, data: [0, 0] });
+      const arbOld = arrayInt64Old({ sign: -1, data: [0, 10] }, { sign: 1, data: [0, 0] });
+      const sourceValue: ArrayInt64 = { sign: -1, data: [0, 8] };
+
+      // Act
+      const treeNew = buildNextShrinkTree(arbNew, new NextValue(sourceValue));
+      const treeOld = buildShrinkTree(arbOld.contextualShrinkableFor(sourceValue));
+
+      // Assert
+      expect(treeOld).toEqual(treeNew);
+    });
+  });
 });
diff --git a/test/unit/check/arbitrary/helpers/BiasNumeric.spec.ts b/test/unit/check/arbitrary/helpers/BiasNumeric.spec.ts
index d61e2123..0095717a 100644
--- a/test/unit/check/arbitrary/helpers/BiasNumeric.spec.ts
+++ b/test/unit/check/arbitrary/helpers/BiasNumeric.spec.ts
@@ -1,27 +1,26 @@
 import * as fc from '../../../../../lib/fast-check';
 
-import { biasNumeric, integerLogLike, bigIntLogLike } from '../../../../../src/check/arbitrary/helpers/BiasNumeric';
+import {
+  retrieveBiasRangesForNumeric,
+  integerLogLike,
+  bigIntLogLike,
+} from '../../../../../src/check/arbitrary/helpers/BiasNumeric';
 
 describe('BiasNumeric', () => {
   describe('biasNumeric', () => {
     it('Should bias close to extreme values and zero if min and max have opposite signs', () =>
       fc.assert(
         fc.property(fc.integer(Number.MIN_SAFE_INTEGER, -1), fc.integer(1, Number.MAX_SAFE_INTEGER), (min, max) => {
-          // Arrange
-          const Ctor = jest.fn().mockImplementation((min, max, genMin, genMax) => {
-            if (genMin > genMax) throw new Error(`Received genMin=${genMin} > genMax=${genMax}`);
-            if (min > genMin) throw new Error(`Received min=${min} > genMin=${genMin}`);
-            if (max < genMax) throw new Error(`Received max=${max} < genMax=${genMax}`);
-          });
-
-          // Act
-          biasNumeric(min, max, Ctor, integerLogLike);
+          // Arrange / Act
+          const ranges = retrieveBiasRangesForNumeric(min, max, integerLogLike);
 
           // Assert
-          expect(Ctor).toHaveBeenCalledTimes(3);
-          expect(Ctor).toHaveBeenCalledWith(min, max, min, expect.any(Number)); // close to min
-          expect(Ctor).toHaveBeenCalledWith(min, max, expect.any(Number), max); // close to max
-          expect(Ctor).toHaveBeenCalledWith(min, max, expect.toBeWithinRange(min, 0), expect.toBeWithinRange(0, max)); // close to zero
+          expect(ranges).toHaveLength(3);
+          expect(ranges).toEqual([
+            { min: expect.toBeWithinRange(min, 0), max: expect.toBeWithinRange(0, max) }, // close to zero
+            { min: expect.toBeWithinRange(0, max), max: max }, // close to max
+            { min: min, max: expect.toBeWithinRange(min, 0) }, // close to min
+          ]);
         })
       ));
 
@@ -34,21 +33,20 @@ describe('BiasNumeric', () => {
           (sign, minRaw, maxRaw) => {
             // Arrange
             fc.pre(minRaw !== maxRaw);
-            const Ctor = jest.fn().mockImplementation((min, max, genMin, genMax) => {
-              if (genMin > genMax) throw new Error(`Received genMin=${genMin} > genMax=${genMax}`);
-              if (min > genMin) throw new Error(`Received min=${min} > genMin=${genMin}`);
-              if (max < genMax) throw new Error(`Received max=${max} < genMax=${genMax}`);
-            });
-            const min = sign * minRaw;
-            const max = sign * maxRaw;
+            const minRawSigned = sign * minRaw;
+            const maxRawSigned = sign * maxRaw;
+            const [min, max] =
+              minRawSigned < maxRawSigned ? [minRawSigned, maxRawSigned] : [maxRawSigned, minRawSigned];
 
             // Act
-            biasNumeric(min, max, Ctor, integerLogLike);
+            const ranges = retrieveBiasRangesForNumeric(min, max, integerLogLike);
 
             // Assert
-            expect(Ctor).toHaveBeenCalledTimes(2);
-            expect(Ctor).toHaveBeenCalledWith(min, max, min, expect.any(Number)); // close to min
-            expect(Ctor).toHaveBeenCalledWith(min, max, expect.any(Number), max); // close to max
+            expect(ranges).toHaveLength(2);
+            const closeToMin = { min: expect.toBeWithinRange(min + 1, max), max: max }; // close to max
+            const closeToMax = { min: min, max: expect.toBeWithinRange(min, max - 1) }; // close to min
+            if (sign > 0) expect(ranges).toEqual([closeToMax, closeToMin]);
+            else expect(ranges).toEqual([closeToMin, closeToMax]);
           }
         )
       ));
@@ -56,19 +54,12 @@ describe('BiasNumeric', () => {
     it('Should not bias anything for equal values of min and max', () =>
       fc.assert(
         fc.property(fc.maxSafeInteger(), (minMax) => {
-          // Arrange
-          const Ctor = jest.fn().mockImplementation((min, max, genMin, genMax) => {
-            if (genMin > genMax) throw new Error(`Received genMin=${genMin} > genMax=${genMax}`);
-            if (min > genMin) throw new Error(`Received min=${min} > genMin=${genMin}`);
-            if (max < genMax) throw new Error(`Received max=${max} < genMax=${genMax}`);
-          });
-
-          // Act
-          biasNumeric(minMax, minMax, Ctor, integerLogLike);
+          // Arrange / Act
+          const ranges = retrieveBiasRangesForNumeric(minMax, minMax, integerLogLike);
 
           // Assert
-          expect(Ctor).toHaveBeenCalledTimes(1);
-          expect(Ctor).toHaveBeenCalledWith(minMax, minMax, minMax, minMax); // no bias, cannot do more
+          expect(ranges).toHaveLength(1);
+          expect(ranges).toEqual([{ min: minMax, max: minMax }]); // no bias, cannot do more
         })
       ));
 
@@ -78,16 +69,19 @@ describe('BiasNumeric', () => {
           // Arrange
           const min = a < b ? a : b;
           const max = a < b ? b : a;
-          const Ctor = jest.fn().mockImplementation((newMin, newMax, genMin, genMax) => {
-            if (newMin !== min) throw new Error(`Received min=${newMin} !== (expected)${min}`);
-            if (newMax !== max) throw new Error(`Received max=${newMax} !== (expected)${max}`);
-            if (genMin > genMax) throw new Error(`Received genMin=${genMin} > genMax=${genMax}`);
-            if (newMin > genMin) throw new Error(`Received min=${newMin} > genMin=${genMin}`);
-            if (newMax < genMax) throw new Error(`Received max=${newMax} < genMax=${genMax}`);
-          });
-
-          // Act / Assert
-          expect(() => biasNumeric(min, max, Ctor, integerLogLike)).not.toThrow();
+
+          // Act
+          const ranges = retrieveBiasRangesForNumeric(min, max, integerLogLike);
+
+          // Assert
+          expect(ranges).not.toHaveLength(0);
+          for (const range of ranges) {
+            expect(range.max).toBeGreaterThanOrEqual(range.min);
+            expect(min).toBeLessThanOrEqual(range.max);
+            expect(max).toBeGreaterThanOrEqual(range.max);
+            expect(min).toBeLessThanOrEqual(range.min);
+            expect(max).toBeGreaterThanOrEqual(range.min);
+          }
         })
       ));
 
@@ -98,16 +92,19 @@ describe('BiasNumeric', () => {
             // Arrange
             const min = a < b ? a : b;
             const max = a < b ? b : a;
-            const Ctor = jest.fn().mockImplementation((newMin, newMax, genMin, genMax) => {
-              if (newMin !== min) throw new Error(`Received min=${newMin} !== (expected)${min}`);
-              if (newMax !== max) throw new Error(`Received max=${newMax} !== (expected)${max}`);
-              if (genMin > genMax) throw new Error(`Received genMin=${genMin} > genMax=${genMax}`);
-              if (newMin > genMin) throw new Error(`Received min=${newMin} > genMin=${genMin}`);
-              if (newMax < genMax) throw new Error(`Received max=${newMax} < genMax=${genMax}`);
-            });
-
-            // Act / Assert
-            expect(() => biasNumeric(min, max, Ctor, bigIntLogLike)).not.toThrow();
+
+            // Act
+            const ranges = retrieveBiasRangesForNumeric(min, max, bigIntLogLike);
+
+            // Assert
+            expect(ranges).not.toHaveLength(0);
+            for (const range of ranges) {
+              expect(range.max).toBeGreaterThanOrEqual(range.min);
+              expect(min).toBeLessThanOrEqual(range.max);
+              expect(max).toBeGreaterThanOrEqual(range.max);
+              expect(min).toBeLessThanOrEqual(range.min);
+              expect(max).toBeGreaterThanOrEqual(range.min);
+            }
           })
         ));
     }
@@ -118,7 +115,7 @@ describe('BiasNumeric', () => {
 
 expect.extend({
   toBeWithinRange(received, floor, ceiling): jest.CustomMatcherResult {
-    const pass = received >= floor && received <= ceiling;
+    const pass = received >= floor && received <= ceiling && !Number.isNaN(received);
     return {
       message: () => `expected ${received} ${pass ? 'not ' : ''} to be within range ${floor} - ${ceiling}`,
       pass,