Repository: dubzzz/fast-check
Pull request #1702 (2021-04-06 21:50:02)

♻️ Migrate letrec 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: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

None expected 🤞 

But if any, some on `letrec` regarding:
- 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/1702/builds/116977) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 0d0c8eb973003e78143059ccd3aac444fc20144c:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-28vk2)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-3rbby)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=h1) Report
> Merging [#1702](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=desc) (0d0c8eb) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/45163e051a1ba10f02c1ff17c322f958874bab34?el=desc) (45163e0) will **decrease** coverage by `0.09%`.
> The diff coverage is `93.75%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1702/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730)](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main    #1702      +/-   ##
==========================================
- Coverage   97.17%   97.07%   -0.10%     
==========================================
  Files          98       98              
  Lines        3504     3492      -12     
  Branches      684      678       -6     
==========================================
- Hits         3405     3390      -15     
- Misses         95       98       +3     
  Partials        4        4              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `97.07% <93.75%> (-0.10%)` | :arrow_down: |
| unit-tests-10.x-Linux | `97.07% <93.75%> (-0.10%)` | :arrow_down: |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/check/arbitrary/LetRecArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1702/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9MZXRSZWNBcmJpdHJhcnkudHM=) | `91.42% <93.75%> (-6.35%)` | :arrow_down: |
| [src/check/arbitrary/ArrayArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1702/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9BcnJheUFyYml0cmFyeS50cw==) | `93.54% <0.00%> (-1.08%)` | :arrow_down: |
| [src/check/arbitrary/MemoArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1702/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9NZW1vQXJiaXRyYXJ5LnRz) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=footer). Last update [45163e0...0d0c8eb](https://codecov.io/gh/dubzzz/fast-check/pull/1702?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/check/arbitrary/LetRecArbitrary.ts b/src/check/arbitrary/LetRecArbitrary.ts
index b20c72f1..6316ae44 100644
--- a/src/check/arbitrary/LetRecArbitrary.ts
+++ b/src/check/arbitrary/LetRecArbitrary.ts
@@ -1,58 +1,39 @@
+import { Stream } from '../../fast-check-default';
 import { Random } from '../../random/generator/Random';
 import { Arbitrary } from './definition/Arbitrary';
-import { Shrinkable } from './definition/Shrinkable';
+import { convertFromNext, convertToNext } from './definition/Converters';
+import { NextArbitrary } from './definition/NextArbitrary';
+import { NextValue } from './definition/NextValue';
 
 /** @internal */
-export class LazyArbitrary extends Arbitrary<any> {
-  private static readonly MaxBiasLevels = 5;
-  private numBiasLevels = 0;
-  private lastBiasedArbitrary: {
-    biasedArb: Arbitrary<any>;
-    arb: Arbitrary<any>;
-    lvl: number;
-    freq: number;
-  } | null = null;
-  underlying: Arbitrary<any> | null = null;
-
+export class LazyArbitrary extends NextArbitrary<any> {
+  underlying: NextArbitrary<any> | null = null;
   constructor(readonly name: string) {
     super();
   }
-  generate(mrng: Random): Shrinkable<any> {
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<any> {
     if (!this.underlying) {
       throw new Error(`Lazy arbitrary ${JSON.stringify(this.name)} not correctly initialized`);
     }
-    return this.underlying.generate(mrng);
+    return this.underlying.generate(mrng, biasFactor);
   }
-  withBias(freq: number): Arbitrary<any> {
+  canGenerate(value: unknown): value is any {
     if (!this.underlying) {
       throw new Error(`Lazy arbitrary ${JSON.stringify(this.name)} not correctly initialized`);
     }
-    if (this.numBiasLevels >= LazyArbitrary.MaxBiasLevels) {
-      return this;
-    }
-    if (
-      this.lastBiasedArbitrary !== null &&
-      this.lastBiasedArbitrary.freq === freq &&
-      this.lastBiasedArbitrary.arb === this.underlying &&
-      this.lastBiasedArbitrary.lvl === this.numBiasLevels
-    ) {
-      return this.lastBiasedArbitrary.biasedArb;
+    return this.underlying.canGenerate(value);
+  }
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shrink(value: any, context?: unknown): Stream<NextValue<any>> {
+    if (!this.underlying) {
+      throw new Error(`Lazy arbitrary ${JSON.stringify(this.name)} not correctly initialized`);
     }
-    ++this.numBiasLevels;
-    const biasedArb = this.underlying.withBias(freq);
-    --this.numBiasLevels;
-    this.lastBiasedArbitrary = {
-      arb: this.underlying,
-      lvl: this.numBiasLevels,
-      freq,
-      biasedArb,
-    };
-    return biasedArb;
+    return this.underlying.shrink(value, context);
   }
 }
 
 /** @internal */
-function isLazyArbitrary(arb: Arbitrary<any> | undefined): arb is LazyArbitrary {
+function isLazyArbitrary(arb: NextArbitrary<any> | undefined): arb is LazyArbitrary {
   return typeof arb === 'object' && arb !== null && Object.prototype.hasOwnProperty.call(arb, 'underlying');
 }
 
@@ -78,12 +59,12 @@ function isLazyArbitrary(arb: Arbitrary<any> | undefined): arb is LazyArbitrary
 export function letrec<T>(
   builder: (tie: (key: string) => Arbitrary<unknown>) => { [K in keyof T]: Arbitrary<T[K]> }
 ): { [K in keyof T]: Arbitrary<T[K]> } {
-  const lazyArbs: { [K in keyof T]?: Arbitrary<T[K]> } = Object.create(null);
+  const lazyArbs: { [K in keyof T]?: NextArbitrary<T[K]> } = Object.create(null);
   const tie = (key: keyof T): Arbitrary<any> => {
     if (!Object.prototype.hasOwnProperty.call(lazyArbs, key)) lazyArbs[key] = new LazyArbitrary(key as any);
     // Call to hasOwnProperty ensures that the property key will be defined
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return lazyArbs[key]!;
+    return convertFromNext(lazyArbs[key]!);
   };
   const strictArbs = builder(tie as any);
   for (const key in strictArbs) {
@@ -93,7 +74,7 @@ export function letrec<T>(
     }
     const lazyAtKey = lazyArbs[key];
     const lazyArb = isLazyArbitrary(lazyAtKey) ? lazyAtKey : new LazyArbitrary(key);
-    lazyArb.underlying = strictArbs[key];
+    lazyArb.underlying = convertToNext(strictArbs[key]);
     lazyArbs[key] = lazyArb;
   }
   return strictArbs;


Test patch:
diff --git a/test/unit/check/arbitrary/LetRecArbitrary.itest.spec.ts b/test/unit/check/arbitrary/LetRecArbitrary.itest.spec.ts
new file mode 100644
index 00000000..3365ddb6
--- /dev/null
+++ b/test/unit/check/arbitrary/LetRecArbitrary.itest.spec.ts
@@ -0,0 +1,73 @@
+import { letrec as letrecOld } from '../../../../src/check/arbitrary/LetRecArbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { FakeIntegerArbitrary } from './generic/NextArbitraryHelpers';
+import {
+  assertGenerateEquivalentTo,
+  assertGenerateProducesSameValueGivenSameSeed,
+  assertGenerateProducesValuesFlaggedAsCanGenerate,
+  assertShrinkProducesSameValueWithoutInitialContext,
+  assertShrinkProducesValuesFlaggedAsCanGenerate,
+} from './generic/NextArbitraryAssertions';
+
+// Temporary rewrapping around letrec
+// Should be removed with next major (no more Arbitrary, only NextArbitrary)
+export function letrec<T>(
+  builder: (tie: (key: string) => NextArbitrary<unknown>) => { [K in keyof T]: NextArbitrary<T[K]> }
+): { [K in keyof T]: NextArbitrary<T[K]> } {
+  const outOld = letrecOld((tieOld) => {
+    const tie: (key: string) => NextArbitrary<unknown> = (key) => convertToNext(tieOld(key));
+    const built = builder(tie);
+    const revampedBuilt: { [K in keyof T]: Arbitrary<T[K]> } = Object.create(null);
+    for (const k of Object.keys(built)) {
+      revampedBuilt[k as keyof T] = convertFromNext(built[k as keyof T]);
+    }
+    return revampedBuilt;
+  });
+  const out: { [K in keyof T]: NextArbitrary<T[K]> } = Object.create(null);
+  for (const k of Object.keys(outOld)) {
+    out[k as keyof T] = convertToNext(outOld[k as keyof T]);
+  }
+  return out;
+}
+
+describe('letrec', () => {
+  const letrecBuilder = () => {
+    const { a } = letrec((tie) => ({
+      a: tie('b'),
+      b: tie('c'),
+      c: new FakeIntegerArbitrary(),
+    }));
+    return a;
+  };
+
+  it('should generate the values as-if we directly called the target arbitrary', () => {
+    assertGenerateEquivalentTo(letrecBuilder, () => new FakeIntegerArbitrary(), {
+      isEqualContext: (c1, c2) => {
+        expect(c2).toEqual(c1);
+      },
+    });
+  });
+
+  it('should generate the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(letrecBuilder);
+  });
+
+  it('should recognize values that would have been generated using it during generate', () => {
+    assertGenerateProducesValuesFlaggedAsCanGenerate(letrecBuilder);
+  });
+
+  it('should shrink towards the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(letrecBuilder);
+  });
+
+  it('should be able to shrink without any context if underlyings do', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(letrecBuilder);
+  });
+
+  it('should recognize values that would have been generated using it during shrink', () => {
+    assertShrinkProducesValuesFlaggedAsCanGenerate(letrecBuilder);
+  });
+});
diff --git a/test/unit/check/arbitrary/LetRecArbitrary.spec.ts b/test/unit/check/arbitrary/LetRecArbitrary.spec.ts
deleted file mode 100644
index d85704b0..00000000
--- a/test/unit/check/arbitrary/LetRecArbitrary.spec.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-import { LazyArbitrary, letrec } from '../../../../src/check/arbitrary/LetRecArbitrary';
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { Shrinkable } from '../../../../src/check/arbitrary/definition/Shrinkable';
-import { Random } from '../../../../src/random/generator/Random';
-
-import * as stubRng from '../../stubs/generators';
-
-describe('LetRecArbitrary', () => {
-  describe('letrec', () => {
-    it('Should be able to construct independant arbitraries', () => {
-      const expectedArb1 = buildArbitrary(jest.fn());
-      const expectedArb2 = buildArbitrary(jest.fn());
-
-      const { arb1, arb2 } = letrec((_tie) => ({
-        arb1: expectedArb1,
-        arb2: expectedArb2,
-      }));
-
-      expect(arb1).toBe(expectedArb1);
-      expect(arb2).toBe(expectedArb2);
-    });
-    it('Should not produce LazyArbitrary for no-tie constructs', () => {
-      const { arb } = letrec((_tie) => ({
-        arb: buildArbitrary(jest.fn()),
-      }));
-      expect(arb).not.toBeInstanceOf(LazyArbitrary);
-    });
-    it('Should not produce LazyArbitrary for indirect tie constructs', () => {
-      const { arb } = letrec((tie) => ({
-        // arb is an arbitrary wrapping the tie value (as fc.array)
-        arb: buildArbitrary((mrng) => tie('arb').generate(mrng)),
-      }));
-      expect(arb).not.toBeInstanceOf(LazyArbitrary);
-    });
-    it('Should produce LazyArbitrary for direct tie constructs', () => {
-      const { arb } = letrec((tie) => ({
-        arb: tie('arb'),
-      }));
-      expect(arb).toBeInstanceOf(LazyArbitrary);
-    });
-    it('Should be able to construct mutually recursive arbitraries', () => {
-      const { arb1, arb2 } = letrec((tie) => ({
-        arb1: tie('arb2'),
-        arb2: tie('arb1'),
-      }));
-      expect(arb1).toBeDefined();
-      expect(arb2).toBeDefined();
-    });
-    it('Should apply tie correctly', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const { arb1, arb2, arb3 } = letrec((tie) => ({
-        arb1: tie('arb2'),
-        arb2: tie('arb3'),
-        arb3: expectedArb,
-      }));
-
-      expect(arb1).toBeInstanceOf(LazyArbitrary);
-      expect(arb2).toBeInstanceOf(LazyArbitrary);
-      expect(arb3).not.toBeInstanceOf(LazyArbitrary);
-
-      expect((arb1 as any).underlying).toBe(arb2);
-      expect((arb2 as any).underlying).toBe(arb3);
-      expect(arb3).toBe(expectedArb);
-    });
-    it('Should be able to delay calls to tie', () => {
-      const mrng = stubRng.mutable.nocall();
-      const generateMock = jest.fn();
-      const simpleArb = buildArbitrary(generateMock);
-      const { arb1 } = letrec((tie) => ({
-        arb1: buildArbitrary((mrng) => tie('arb2').generate(mrng)),
-        arb2: simpleArb,
-      }));
-
-      expect(generateMock).not.toHaveBeenCalled();
-      arb1.generate(mrng);
-
-      expect(generateMock).toHaveBeenCalled();
-    });
-    it('Should throw on generate if tie receives an invalid parameter', () => {
-      const mrng = stubRng.mutable.nocall();
-      const { arb1 } = letrec((tie) => ({
-        arb1: tie('missing'),
-      }));
-      expect(() => arb1.generate(mrng)).toThrowErrorMatchingSnapshot();
-    });
-    it('Should throw on generate if tie receives an invalid parameter after creation', () => {
-      const mrng = stubRng.mutable.nocall();
-      const { arb1 } = letrec((tie) => ({
-        arb1: buildArbitrary((mrng) => tie('missing').generate(mrng)),
-      }));
-      expect(() => arb1.generate(mrng)).toThrowErrorMatchingSnapshot();
-    });
-    it('Should apply tie correctly', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const { arb1, arb2, arb3 } = letrec((tie) => ({
-        arb1: tie('arb2'),
-        arb2: tie('arb3'),
-        arb3: expectedArb,
-      }));
-
-      expect(arb1).toBeInstanceOf(LazyArbitrary);
-      expect(arb2).toBeInstanceOf(LazyArbitrary);
-      expect(arb3).not.toBeInstanceOf(LazyArbitrary);
-
-      expect((arb1 as any).underlying).toBe(arb2);
-      expect((arb2 as any).underlying).toBe(arb3);
-      expect(arb3).toBe(expectedArb);
-    });
-    it('Should accept "reserved" keys', () => {
-      const mrng = stubRng.mutable.nocall();
-      const generateMock = jest.fn();
-      const simpleArb = buildArbitrary(generateMock);
-      const { tie } = letrec((tie) => ({
-        tie: tie('__proto__'),
-        ['__proto__']: tie('__defineGetter__​​'),
-        ['__defineGetter__​​']: tie('__defineSetter__​​'),
-        ['__defineSetter__​​']: tie('__lookupGetter__​​'),
-        ['__lookupGetter__​​']: tie('__lookupSetter__​​'),
-        ['__lookupSetter__​​']: tie('constructor​​'),
-        ['constructor​​']: tie('hasOwnProperty​​'),
-        ['hasOwnProperty​​']: tie('isPrototypeOf​​'),
-        ['isPrototypeOf​​']: tie('propertyIsEnumerable​​'),
-        ['propertyIsEnumerable​​']: tie('toLocaleString​​'),
-        ['toLocaleString​​']: tie('toSource​​'),
-        ['toSource​​']: tie('toString​​'),
-        ['toString​​']: tie('valueOf'),
-        ['valueOf']: simpleArb,
-      }));
-
-      expect(generateMock).not.toHaveBeenCalled();
-      tie.generate(mrng);
-
-      expect(generateMock).toHaveBeenCalled();
-    });
-    it('Should accept builders producing objects based on Object.create(null)', () => {
-      const mrng = stubRng.mutable.nocall();
-      const generateMock = jest.fn();
-      const simpleArb = buildArbitrary(generateMock);
-      const { a } = letrec((tie) =>
-        Object.assign(Object.create(null), {
-          a: tie('b'),
-          b: simpleArb,
-        })
-      );
-
-      expect(generateMock).not.toHaveBeenCalled();
-      a.generate(mrng);
-
-      expect(generateMock).toHaveBeenCalled();
-    });
-  });
-  describe('LazyArbitrary', () => {
-    it('Should fail to generate when no underlying arbitrary', () => {
-      const mrng = stubRng.mutable.nocall();
-      const lazy = new LazyArbitrary('id007');
-      expect(() => lazy.generate(mrng)).toThrowErrorMatchingSnapshot();
-    });
-    it('Should fail to bias when no underlying arbitrary', () => {
-      const lazy = new LazyArbitrary('id008');
-      expect(() => lazy.withBias(2)).toThrowErrorMatchingSnapshot();
-    });
-    it('Should call generate method of underlying on generate', () => {
-      const mrng = stubRng.mutable.nocall();
-      const lazy = new LazyArbitrary('id008');
-      const expectedGen = Symbol();
-      const generateMock = jest.fn();
-      generateMock.mockReturnValue(expectedGen);
-      lazy.underlying = buildArbitrary(generateMock);
-
-      const g = lazy.generate(mrng);
-      expect(g).toBe(expectedGen);
-      expect(generateMock).toHaveBeenCalledTimes(1);
-    });
-    it('Should be able to bias to the biased value of underlying', () => {
-      const lazy = new LazyArbitrary('id008');
-      const noCallMock = jest.fn();
-      const biasedArb = buildArbitrary(noCallMock);
-      const arb = buildArbitrary(noCallMock, () => biasedArb);
-      lazy.underlying = arb;
-
-      const biasedLazy = lazy.withBias(2);
-      expect(biasedLazy).toBe(biasedArb);
-      expect(noCallMock).not.toHaveBeenCalled();
-    });
-    it('Should be able to bias recursive arbitraries', () => {
-      const lazy = new LazyArbitrary('id008');
-      const noCallMock = jest.fn();
-      lazy.underlying = buildArbitrary(noCallMock, (n) => lazy.withBias(n));
-
-      const biasedLazy = lazy.withBias(2);
-      expect(biasedLazy).toBe(lazy);
-      expect(noCallMock).not.toHaveBeenCalled();
-    });
-    it('Should bias recursive arbitraries at a max depth of 5', () => {
-      const lazyA = new LazyArbitrary('id007');
-      const lazyB = new LazyArbitrary('id008');
-
-      const noCallMock = jest.fn();
-      const biasAMock = jest.fn();
-      const biasBMock = jest.fn();
-      biasAMock.mockImplementation((n) => lazyB.withBias(n));
-      biasBMock.mockImplementation((n) => lazyB.withBias(n));
-      lazyA.underlying = buildArbitrary(noCallMock, biasAMock);
-      lazyB.underlying = buildArbitrary(noCallMock, biasBMock);
-
-      const biasedLazyA = lazyA.withBias(2);
-      expect(biasedLazyA).toBe(lazyB);
-      expect(biasAMock).toHaveBeenCalledTimes(1);
-      expect(biasBMock).toHaveBeenCalledTimes(5);
-    });
-    it('Should cache biased arbitrary for same freq', () => {
-      const lazy = new LazyArbitrary('id007');
-
-      const biasMock = jest.fn();
-      biasMock.mockImplementation((n) => lazy.withBias(n));
-      lazy.underlying = buildArbitrary(jest.fn(), biasMock);
-
-      lazy.withBias(42);
-      biasMock.mockClear();
-
-      lazy.withBias(42);
-      expect(biasMock).not.toBeCalled();
-    });
-    it('Should not take from cache if freq changed', () => {
-      const lazy = new LazyArbitrary('id007');
-
-      const biasMock = jest.fn();
-      biasMock.mockImplementation((n) => lazy.withBias(n));
-      lazy.underlying = buildArbitrary(jest.fn(), biasMock);
-
-      lazy.withBias(42);
-      biasMock.mockClear();
-
-      lazy.withBias(69);
-      expect(biasMock).toBeCalled();
-    });
-  });
-});
-
-const buildArbitrary = (generate: (mrng: Random) => Shrinkable<any>, withBias?: (n: number) => Arbitrary<any>) => {
-  return new (class extends Arbitrary<any> {
-    generate(mrng: Random) {
-      return generate(mrng);
-    }
-    withBias(n: number): Arbitrary<any> {
-      return withBias ? withBias(n) : this;
-    }
-  })();
-};
diff --git a/test/unit/check/arbitrary/LetRecArbitrary.utest.spec.ts b/test/unit/check/arbitrary/LetRecArbitrary.utest.spec.ts
new file mode 100644
index 00000000..0899841a
--- /dev/null
+++ b/test/unit/check/arbitrary/LetRecArbitrary.utest.spec.ts
@@ -0,0 +1,340 @@
+import { LazyArbitrary, letrec as letrecOld } from '../../../../src/check/arbitrary/LetRecArbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+
+import * as stubRng from '../../stubs/generators';
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { fakeNextArbitrary } from './generic/NextArbitraryHelpers';
+import { Stream } from '../../../../src/stream/Stream';
+
+const mrngNoCall = stubRng.mutable.nocall();
+
+// Temporary rewrapping around letrec
+// Should be removed with next major (no more Arbitrary, only NextArbitrary)
+export function letrec<T>(
+  builder: (tie: (key: string) => NextArbitrary<unknown>) => { [K in keyof T]: NextArbitrary<T[K]> }
+): { [K in keyof T]: NextArbitrary<T[K]> } {
+  const outOld = letrecOld((tieOld) => {
+    const tie: (key: string) => NextArbitrary<unknown> = (key) => convertToNext(tieOld(key));
+    const built = builder(tie);
+    const revampedBuilt: { [K in keyof T]: Arbitrary<T[K]> } = Object.create(null);
+    for (const k of Object.keys(built)) {
+      revampedBuilt[k as keyof T] = convertFromNext(built[k as keyof T]);
+    }
+    return revampedBuilt;
+  });
+  const out: { [K in keyof T]: NextArbitrary<T[K]> } = Object.create(null);
+  for (const k of Object.keys(outOld)) {
+    out[k as keyof T] = convertToNext(outOld[k as keyof T]);
+  }
+  return out;
+}
+
+describe('letrec', () => {
+  describe('builder', () => {
+    it('should be able to construct independant arbitraries', () => {
+      // Arrange
+      const { instance: expectedArb1 } = fakeNextArbitrary();
+      const { instance: expectedArb2 } = fakeNextArbitrary();
+
+      // Act
+      const { arb1, arb2 } = letrec((_tie) => ({
+        arb1: expectedArb1,
+        arb2: expectedArb2,
+      }));
+
+      // Assert
+      expect(arb1).toBe(expectedArb1);
+      expect(arb2).toBe(expectedArb2);
+    });
+
+    it('should not produce LazyArbitrary for no-tie constructs', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+
+      // Act
+      const { arb } = letrec((_tie) => ({
+        arb: expectedArb,
+      }));
+
+      // Assert
+      expect(arb).not.toBeInstanceOf(LazyArbitrary);
+    });
+
+    it('should not produce LazyArbitrary for indirect tie constructs', () => {
+      // Arrange / Act
+      const { arb } = letrec((tie) => {
+        const { instance: expectedArb, generate } = fakeNextArbitrary();
+        generate.mockImplementation((...args) => tie('arb').generate(...args));
+        return {
+          // arb is an arbitrary wrapping the tie value (as fc.array)
+          arb: expectedArb,
+        };
+      });
+
+      // Assert
+      expect(arb).not.toBeInstanceOf(LazyArbitrary);
+    });
+
+    it('should produce LazyArbitrary for direct tie constructs', () => {
+      // Arrange / Act
+      const { arb } = letrec((tie) => ({
+        arb: tie('arb'),
+      }));
+
+      // Assert
+      expect(arb).toBeInstanceOf(LazyArbitrary);
+    });
+
+    it('should be able to construct mutually recursive arbitraries', () => {
+      // Arrange / Act
+      const { arb1, arb2 } = letrec((tie) => ({
+        arb1: tie('arb2'),
+        arb2: tie('arb1'),
+      }));
+
+      // Assert
+      expect(arb1).toBeDefined();
+      expect(arb2).toBeDefined();
+    });
+
+    it('should apply tie correctly', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+
+      // Act
+      const { arb1, arb2, arb3 } = letrec((tie) => ({
+        arb1: tie('arb2'),
+        arb2: tie('arb3'),
+        arb3: expectedArb,
+      }));
+
+      // Assert
+      expect(arb1).toBeInstanceOf(LazyArbitrary);
+      expect(arb2).toBeInstanceOf(LazyArbitrary);
+      expect(arb3).not.toBeInstanceOf(LazyArbitrary);
+      expect(((arb1 as any) as LazyArbitrary).underlying).toBe(arb2);
+      expect(((arb2 as any) as LazyArbitrary).underlying).toBe(arb3);
+      expect(arb3).toBe(expectedArb);
+    });
+  });
+
+  describe('generate', () => {
+    it('should be able to delay calls to tie to generate', () => {
+      // Arrange
+      const biasFactor = 69;
+      const { instance: simpleArb, generate } = fakeNextArbitrary();
+      generate.mockReturnValueOnce(new NextValue(null));
+
+      // Act
+      const { arb1 } = letrec((tie) => {
+        const { instance: simpleArb2, generate: generate2 } = fakeNextArbitrary();
+        generate2.mockImplementation((...args) => tie('arb2').generate(...args));
+        return {
+          arb1: simpleArb2,
+          arb2: simpleArb,
+        };
+      });
+      expect(generate).not.toHaveBeenCalled();
+      arb1.generate(mrngNoCall, biasFactor);
+
+      // Assert
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, biasFactor);
+    });
+
+    it('should throw on generate if tie receives an invalid parameter', () => {
+      // Arrange
+      const biasFactor = 42;
+      const { arb1 } = letrec((tie) => ({
+        arb1: tie('missing'),
+      }));
+
+      // Act / Assert
+      expect(() => arb1.generate(mrngNoCall, biasFactor)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"missing\\" not correctly initialized"`
+      );
+    });
+
+    it('should throw on generate if tie receives an invalid parameter after creation', () => {
+      // Arrange
+      const biasFactor = 42;
+      const { arb1 } = letrec((tie) => {
+        const { instance: simpleArb, generate } = fakeNextArbitrary();
+        generate.mockImplementation((...args) => tie('missing').generate(...args));
+        return {
+          arb1: simpleArb,
+        };
+      });
+
+      // Act / Assert
+      expect(() => arb1.generate(mrngNoCall, biasFactor)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"missing\\" not correctly initialized"`
+      );
+    });
+
+    it('should accept "reserved" keys as output of builder', () => {
+      // Arrange
+      const biasFactor = 42;
+      const { instance: simpleArb, generate } = fakeNextArbitrary();
+      generate.mockReturnValueOnce(new NextValue(null));
+      const { tie } = letrec((tie) => ({
+        tie: tie('__proto__'),
+        ['__proto__']: tie('__defineGetter__​​'),
+        ['__defineGetter__​​']: tie('__defineSetter__​​'),
+        ['__defineSetter__​​']: tie('__lookupGetter__​​'),
+        ['__lookupGetter__​​']: tie('__lookupSetter__​​'),
+        ['__lookupSetter__​​']: tie('constructor​​'),
+        ['constructor​​']: tie('hasOwnProperty​​'),
+        ['hasOwnProperty​​']: tie('isPrototypeOf​​'),
+        ['isPrototypeOf​​']: tie('propertyIsEnumerable​​'),
+        ['propertyIsEnumerable​​']: tie('toLocaleString​​'),
+        ['toLocaleString​​']: tie('toSource​​'),
+        ['toSource​​']: tie('toString​​'),
+        ['toString​​']: tie('valueOf'),
+        ['valueOf']: simpleArb,
+      }));
+
+      // Act
+      expect(generate).not.toHaveBeenCalled();
+      tie.generate(mrngNoCall, biasFactor);
+
+      // Assert
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, biasFactor);
+    });
+
+    it('Should accept builders producing objects based on Object.create(null)', () => {
+      // Arrange
+      const biasFactor = 42;
+      const { instance: simpleArb, generate } = fakeNextArbitrary();
+      generate.mockReturnValueOnce(new NextValue(null));
+      const { a } = letrec((tie) =>
+        Object.assign(Object.create(null), {
+          a: tie('b'),
+          b: simpleArb,
+        })
+      );
+
+      // Act
+      expect(generate).not.toHaveBeenCalled();
+      a.generate(mrngNoCall, biasFactor);
+
+      // Assert
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, biasFactor);
+    });
+  });
+
+  describe('canGenerate', () => {
+    it.each`
+      expectedStatus
+      ${false}
+      ${true}
+    `('should call canGenerate on the targets', ({ expectedStatus }) => {
+      // Arrange
+      const expectedValue = Symbol();
+      const { instance: simpleArb, canGenerate } = fakeNextArbitrary();
+      canGenerate.mockReturnValueOnce(expectedStatus);
+      const { arb1 } = letrec((tie) => {
+        return {
+          arb1: tie('arb2'),
+          arb2: simpleArb,
+        };
+      });
+
+      // Act
+      const out = arb1.canGenerate(expectedValue);
+
+      // Assert
+      expect(canGenerate).toHaveBeenCalledTimes(1);
+      expect(canGenerate).toHaveBeenCalledWith(expectedValue);
+      expect(out).toBe(expectedStatus);
+    });
+
+    it('should throw on canGenerate if tie receives an invalid parameter', () => {
+      // Arrange
+      const expectedValue = Symbol();
+      const { arb1 } = letrec((tie) => ({
+        arb1: tie('missing'),
+      }));
+
+      // Act / Assert
+      expect(() => arb1.canGenerate(expectedValue)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"missing\\" not correctly initialized"`
+      );
+    });
+  });
+
+  describe('shrink', () => {
+    it('should call shrink on the targets', () => {
+      // Arrange
+      const expectedValue = Symbol();
+      const expectedContext = Symbol();
+      const expectedStream = Stream.of(new NextValue(Symbol()));
+      const { instance: simpleArb, shrink } = fakeNextArbitrary();
+      shrink.mockReturnValueOnce(expectedStream);
+      const { arb1 } = letrec((tie) => {
+        return {
+          arb1: tie('arb2'),
+          arb2: simpleArb,
+        };
+      });
+
+      // Act
+      const out = arb1.shrink(expectedValue, expectedContext);
+
+      // Assert
+      expect(shrink).toHaveBeenCalledTimes(1);
+      expect(shrink).toHaveBeenCalledWith(expectedValue, expectedContext);
+      expect(out).toBe(expectedStream);
+    });
+
+    it('should throw on shrink if tie receives an invalid parameter', () => {
+      // Arrange
+      const expectedValue = Symbol();
+      const expectedContext = Symbol();
+      const { arb1 } = letrec((tie) => ({
+        arb1: tie('missing'),
+      }));
+
+      // Act / Assert
+      expect(() => arb1.shrink(expectedValue, expectedContext)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"missing\\" not correctly initialized"`
+      );
+    });
+  });
+
+  describe('LazyArbitrary (internal)', () => {
+    it('should fail to generate when no underlying arbitrary', () => {
+      // Arrange / Act
+      const lazy = new LazyArbitrary('id007');
+
+      // Assert
+      expect(() => lazy.generate(mrngNoCall, 2)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"id007\\" not correctly initialized"`
+      );
+    });
+
+    it('should fail to check canGenerate when no underlying arbitrary', () => {
+      // Arrange / Act
+      const lazy = new LazyArbitrary('id007');
+
+      // Assert
+      expect(() => lazy.canGenerate(1)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"id007\\" not correctly initialized"`
+      );
+    });
+
+    it('should fail to shrink when no underlying arbitrary', () => {
+      // Arrange / Act
+      const lazy = new LazyArbitrary('id007');
+
+      // Assert
+      expect(() => lazy.shrink(1, 2)).toThrowErrorMatchingInlineSnapshot(
+        `"Lazy arbitrary \\"id007\\" not correctly initialized"`
+      );
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/__snapshots__/LetRecArbitrary.spec.ts.snap b/test/unit/check/arbitrary/__snapshots__/LetRecArbitrary.spec.ts.snap
deleted file mode 100644
index 2740e548..00000000
--- a/test/unit/check/arbitrary/__snapshots__/LetRecArbitrary.spec.ts.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`LetRecArbitrary LazyArbitrary Should fail to bias when no underlying arbitrary 1`] = `"Lazy arbitrary \\"id008\\" not correctly initialized"`;
-
-exports[`LetRecArbitrary LazyArbitrary Should fail to generate when no underlying arbitrary 1`] = `"Lazy arbitrary \\"id007\\" not correctly initialized"`;
-
-exports[`LetRecArbitrary letrec Should throw on generate if tie receives an invalid parameter 1`] = `"Lazy arbitrary \\"missing\\" not correctly initialized"`;
-
-exports[`LetRecArbitrary letrec Should throw on generate if tie receives an invalid parameter after creation 1`] = `"Lazy arbitrary \\"missing\\" not correctly initialized"`;
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts b/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
index f7f5134b..c504ca0c 100644
--- a/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
+++ b/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
@@ -153,6 +153,42 @@ export function assertShrinkProducesCorrectValues<T, U = never>(
   );
 }
 
+export function assertGenerateEquivalentTo<T, U = never>(
+  arbitraryBuilderA: (extraParameters: U) => NextArbitrary<T>,
+  arbitraryBuilderB: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    isEqualContext?: (c1: unknown, c2: unknown, extraParameters: U) => void | boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    isEqual,
+    isEqualContext,
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(fc.integer().noShrink(), biasFactorArbitrary(), extra, (seed, biasFactor, extraParameters) => {
+      // Arrange
+      const arbA = arbitraryBuilderA(extraParameters);
+      const arbB = arbitraryBuilderB(extraParameters);
+
+      // Act
+      const gA = arbA.generate(randomFromSeed(seed), biasFactor);
+      const gB = arbB.generate(randomFromSeed(seed), biasFactor);
+
+      // Assert
+      assertEquality(isEqual, gA.value, gB.value, extraParameters);
+      if (isEqualContext) {
+        assertEquality(isEqualContext, gA.context, gB.context, extraParameters);
+      }
+    }),
+    assertParameters
+  );
+}
+
 // Extra requirements
 // The assertions above can be configured to push generators even further. They ensure more complex invariants.
 // Following assertions are mostly derived from the one above.