Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 140 additions & 24 deletions packages/fast-check/src/arbitrary/letrec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { LazyArbitrary } from './_internals/LazyArbitrary';
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { safeHasOwnProperty } from '../utils/globals';
import { nat } from './nat.js';
import { record } from './record.js';
import { array } from './array.js';

const safeArrayIsArray = Array.isArray;
const safeObjectCreate = Object.create;
const safeObjectEntries = Object.entries;

/**
* Type of the value produced by {@link letrec}
Expand Down Expand Up @@ -50,6 +55,131 @@
*/
export type LetrecLooselyTypedBuilder<T> = (tie: LetrecLooselyTypedTie) => LetrecValue<T>;

/**
* Constraints to be applied on {@link letrec}
* @remarks Since 4.2.0
* @public
*/
export interface LetrecConstraints {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do that in a follow up PR so that we unlock this one:

We should import and export it from fast-check-default.ts so that end users can import it too.

It should be a type rather than an interface to fest with what we currently do for all others (to be confirmed, I have rechecked).

/**
* Generate objects with circular references
* @defaultValue false
* @remarks Since 4.2.0
*/
circular?: boolean;
}

function nonCircularLetrec<T>(builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>): LetrecValue<T> {
const lazyArbs: { [K in keyof T]?: LazyArbitrary<unknown> } = safeObjectCreate(null);
const tie = (key: keyof T): Arbitrary<any> => {
if (!safeHasOwnProperty(lazyArbs, key)) {
// Call to hasOwnProperty ensures that the property key will be defined
lazyArbs[key] = new LazyArbitrary(String(key));
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return lazyArbs[key]!;
};
const strictArbs = builder(tie as any);
for (const key in strictArbs) {
if (!safeHasOwnProperty(strictArbs, key)) {
// Prevents accidental iteration over properties inherited from an object’s prototype
continue;
}

Check warning on line 87 in packages/fast-check/src/arbitrary/letrec.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/letrec.ts#L86-L87

Added lines #L86 - L87 were not covered by tests
const lazyAtKey: LazyArbitrary<unknown> | undefined = lazyArbs[key];
const lazyArb = lazyAtKey !== undefined ? lazyAtKey : new LazyArbitrary(key);
lazyArb.underlying = strictArbs[key];
lazyArbs[key] = lazyArb;
}
return strictArbs;
}

function circularLetrec<T>(builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>): LetrecValue<T> {
const lazyArbs: { [K in keyof T]?: LazyArbitrary<unknown> } = safeObjectCreate(null);
const tie = (key: keyof T): Arbitrary<any> => {
if (!safeHasOwnProperty(lazyArbs, key)) {
// Call to hasOwnProperty ensures that the property key will be defined
lazyArbs[key] = new LazyArbitrary(String(key));
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return lazyArbs[key]!;
};
const strictArbs = builder(tie as any);

// Symbol to replace with a potentially circular reference later.
const placeholderSymbol = Symbol('placeholder');
const poolArbs: { [K in keyof T]: Arbitrary<unknown[]> } = safeObjectCreate(null);
for (const key in strictArbs) {
if (!safeHasOwnProperty(strictArbs, key)) {
// Prevents accidental iteration over properties inherited from an object’s prototype
continue;
}

Check warning on line 115 in packages/fast-check/src/arbitrary/letrec.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/letrec.ts#L114-L115

Added lines #L114 - L115 were not covered by tests
const lazyAtKey: LazyArbitrary<unknown> | undefined = lazyArbs[key];
const lazyArb = lazyAtKey !== undefined ? lazyAtKey : new LazyArbitrary(key);
lazyArb.underlying = nat().map((index) => ({
[placeholderSymbol]: { key, index },
}));
lazyArbs[key] = lazyArb;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
poolArbs[key] = array(strictArbs[key]!, { minLength: 1 });
}

for (const key in strictArbs) {
if (!safeHasOwnProperty(strictArbs, key)) {
// Prevents accidental iteration over properties inherited from an object’s prototype
continue;
}

Check warning on line 130 in packages/fast-check/src/arbitrary/letrec.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/letrec.ts#L129-L130

Added lines #L129 - L130 were not covered by tests

const poolsArb = record(poolArbs as any) as Arbitrary<{ [K in keyof T]: unknown[] }>;
strictArbs[key] = poolsArb.map((pools) => {
const visited = new WeakSet();
function deref(value: unknown, source?: Record<PropertyKey, unknown>, sourceKey?: PropertyKey): unknown {
if (typeof value !== 'object' || value === null) {
return value;
}

if (visited.has(value)) {
return value;
}
visited.add(value);

if (safeHasOwnProperty(value, placeholderSymbol)) {
const { key, index } = (
value as {
[placeholderSymbol]: { key: keyof T; index: number };
}
)[placeholderSymbol];
const pool = pools[key];
const poolValue = pool[index % pool.length];
if (source !== undefined && sourceKey !== undefined) {
source[sourceKey] = poolValue;
return value;
} else {
return poolValue;
}

Check warning on line 158 in packages/fast-check/src/arbitrary/letrec.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/letrec.ts#L157-L158

Added lines #L157 - L158 were not covered by tests
}

if (safeArrayIsArray(value)) {
for (let i = 0; i < value.length; i++) {
deref(value[i], value as unknown as Record<PropertyKey, unknown>, i);
}
} else {
for (const [key, item] of safeObjectEntries(value)) {
deref(item, value as Record<PropertyKey, unknown>, key);
}
}

return value;
}

// TODO: Do we need to clone here?
deref(pools);
return pools[key][0];
}) as (typeof strictArbs)[typeof key];
}

return strictArbs;
}

/**
* For mutually recursive types
*
Expand All @@ -72,7 +202,10 @@
* @remarks Since 1.16.0
* @public
*/
export function letrec<T>(builder: T extends Record<string, unknown> ? LetrecTypedBuilder<T> : never): LetrecValue<T>;
export function letrec<T>(
builder: T extends Record<string, unknown> ? LetrecTypedBuilder<T> : never,
constraints?: LetrecConstraints,
): LetrecValue<T>;
/**
* For mutually recursive types
*
Expand All @@ -92,27 +225,10 @@
* @remarks Since 1.16.0
* @public
*/
export function letrec<T>(builder: LetrecLooselyTypedBuilder<T>): LetrecValue<T>;
export function letrec<T>(builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>): LetrecValue<T> {
const lazyArbs: { [K in keyof T]?: LazyArbitrary<unknown> } = safeObjectCreate(null);
const tie = (key: keyof T): Arbitrary<any> => {
if (!safeHasOwnProperty(lazyArbs, key)) {
// Call to hasOwnProperty ensures that the property key will be defined
lazyArbs[key] = new LazyArbitrary(String(key));
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return lazyArbs[key]!;
};
const strictArbs = builder(tie as any);
for (const key in strictArbs) {
if (!safeHasOwnProperty(strictArbs, key)) {
// Prevents accidental iteration over properties inherited from an object’s prototype
continue;
}
const lazyAtKey: LazyArbitrary<unknown> | undefined = lazyArbs[key];
const lazyArb = lazyAtKey !== undefined ? lazyAtKey : new LazyArbitrary(key);
lazyArb.underlying = strictArbs[key];
lazyArbs[key] = lazyArb;
}
return strictArbs;
export function letrec<T>(builder: LetrecLooselyTypedBuilder<T>, constraints?: LetrecConstraints): LetrecValue<T>;
export function letrec<T>(
builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>,
constraints: LetrecConstraints = {},
): LetrecValue<T> {
return (constraints.circular ? circularLetrec : nonCircularLetrec)(builder);
}
45 changes: 45 additions & 0 deletions packages/fast-check/test/unit/arbitrary/letrec.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { describe, it, expect } from 'vitest';
import { letrec } from '../../../src/arbitrary/letrec';
import { record } from '../../../src/arbitrary/record';
import { LazyArbitrary } from '../../../src/arbitrary/_internals/LazyArbitrary';
import { Value } from '../../../src/check/arbitrary/definition/Value';
import { Stream } from '../../../src/stream/Stream';
import { FakeIntegerArbitrary, fakeArbitrary } from './__test-helpers__/ArbitraryHelpers';
import { fakeRandom } from './__test-helpers__/RandomHelpers';
import {
assertGenerateEquivalentTo,
assertProduceCorrectValues,
assertProduceSameValueGivenSameSeed,
assertProduceValuesShrinkableWithoutContext,
assertShrinkProducesSameValueWithoutInitialContext,
Expand Down Expand Up @@ -347,3 +349,46 @@ describe('letrec (integration)', () => {
assertShrinkProducesSameValueWithoutInitialContext(letrecBuilder);
});
});

describe('letrec circular (integration)', () => {
type Node = {
value: number;
next: Node;
};
const letrecBuilder = () => {
const { node } = letrec<{ node: Node }>(
(tie) => ({
node: record({
value: new FakeIntegerArbitrary(),
next: tie('node'),
}),
}),
{ circular: true },
);
return node;
};

it('should produce the same values given the same seed', () => {
assertProduceSameValueGivenSameSeed(letrecBuilder);
});

it('should only produce correct values', () => {
assertProduceCorrectValues(letrecBuilder, (node) => {
let circular = false;
const visited = new WeakSet();
const assertNode = (node: Node) => {
if (visited.has(node)) {
circular = true;
return;
}
visited.add(node);
expect(typeof node.value).toBe('number');
assertNode(node.next);
};
assertNode(node);
// Must be circular because `next` isn't optional, so it has to circle
// around eventually.
expect(circular).toBe(true);
});
});
});
14 changes: 11 additions & 3 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
packages:
- 'examples'
- 'packages/*'
- 'website'
- examples
- packages/*
- website

onlyBuiltDependencies:
- '@swc/core'
- core-js
- core-js-pure
- es5-ext
- esbuild
- unrs-resolver
Loading