Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,19 @@ export function resolveSize(size: Exclude<SizeForArbitrary, 'max'> | undefined):
}
return relativeSizeToSize(size, defaultSize);
}

/** @internal */
export function invertSize(size: Size): Size {
switch (size) {
case 'xsmall':
return 'xlarge';
case 'small':
return 'large';
case 'medium':
return 'medium';
case 'large':
return 'small';
case 'xlarge':
return 'xsmall';
}
}
193 changes: 168 additions & 25 deletions packages/fast-check/src/arbitrary/letrec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { LazyArbitrary } from './_internals/LazyArbitrary';
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { safeHasOwnProperty } from '../utils/globals';
import { safeAdd, safeHas, safeHasOwnProperty } from '../utils/globals';
import { invertSize, resolveSize, type SizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';
import { nat } from './nat.js';
import { record } from './record.js';
import { array } from './array.js';
import { noShrink } from './noShrink.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 +57,154 @@ export type LetrecLooselyTypedTie = (key: string) => Arbitrary<unknown>;
*/
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
*/
withCycles?: boolean | CycleConstraints;
}

/**
* Constraints to be applied on {@link LetrecConstraints.withCycles}
* @remarks Since 4.2.0
* @public
*/
export interface CycleConstraints {
/**
* Define how frequently cycles should occur in the generated values (at max)
* @remarks Since 4.2.0
*/
frequencySize?: Exclude<SizeForArbitrary, 'max'>;
}

/** @internal */
function letrecWithoutCycles<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;
}

/** @internal */
export function derefPools<T>(pools: { [K in keyof T]: unknown[] }, placeholderSymbol: symbol): void {
const visited = new Set();
function deref(value: unknown, source?: Record<PropertyKey, unknown>, sourceKey?: PropertyKey) {
if (typeof value !== 'object' || value === null) {
return;
}

if (safeHas(visited, value)) {
return;
}
safeAdd(visited, value);

if (safeHasOwnProperty(value, placeholderSymbol)) {
// This is a while loop because it's possible for an arbitrary to be defined as just `arb: tie('otherArb')`, in
// which case what the `arb` generates is also a placeholder.
let currentValue: unknown = value;
do {
const { key, index } = (currentValue as { [placeholderSymbol]: { key: keyof T; index: number } })[
placeholderSymbol
];
const pool = pools[key];
currentValue = pool[index % pool.length];
if (source !== undefined && sourceKey !== undefined) {
source[sourceKey] = currentValue;
}
} while (safeHasOwnProperty(currentValue, placeholderSymbol));
return;
}

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);
}
}
}
deref(pools);
}

/** @internal */
function letrecWithCycles<T>(
builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>,
constraints: CycleConstraints,
): 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);
const poolConstraints = {
minLength: 1,
// Higher cycle frequency is achieved by using a smaller pool of objects, so we invert the input `frequency`.
size: invertSize(resolveSize(constraints.frequencySize)),
};
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 = noShrink(nat().map((index) => ({ [placeholderSymbol]: { key, index } })));
lazyArbs[key] = lazyArb;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
poolArbs[key] = array(strictArbs[key]!, poolConstraints);
}
Comment on lines +179 to +190
Copy link
Owner

Choose a reason for hiding this comment

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

Personal note: Close to the non-cycle one. The difference is that non-cycle one was doing lazyArb.underlying = strictArbs[key]. Plus the pool.

Copy link
Owner

Choose a reason for hiding this comment

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

I fell that up to this loop we could have the two codes being the same. The "with cycles" one could just be a follow up code not returning immediately but doing the extra stuff with pools and cie.


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

const poolsArb = record(poolArbs as any) as Arbitrary<{ [K in keyof T]: unknown[] }>;
strictArbs[key] = poolsArb.map((pools) => {
derefPools(pools, placeholderSymbol);
return pools[key][0];
}) as (typeof strictArbs)[typeof key];
}

return strictArbs;
}

/**
* For mutually recursive types
*
Expand All @@ -72,7 +227,10 @@ export type LetrecLooselyTypedBuilder<T> = (tie: LetrecLooselyTypedTie) => Letre
* @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 +250,12 @@ export function letrec<T>(builder: T extends Record<string, unknown> ? LetrecTyp
* @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.withCycles
? letrecWithCycles(builder, constraints.withCycles === true ? {} : constraints.withCycles)
: letrecWithoutCycles(builder);
}
Loading
Loading