Skip to content

Commit e4b8d29

Browse files
✨ Add circular option to fc.letrec (#6040)
**Description** Closes #6039 **Checklist** — _Don't delete this checklist and make sure you do the following before opening the PR_ - [x] The name of my PR follows [gitmoji](https://gitmoji.dev/) specification - [x] My PR references one of several related issues (if any) - [x] New features or breaking changes must come with an associated Issue or Discussion - [x] My PR does not add any new dependency without an associated Issue or Discussion - [ ] My PR includes bumps details, please run `pnpm run bump` and flag the impacts properly - [ ] My PR adds relevant tests and they would have failed without my PR (when applicable) <!-- More about contributing at https://github.com/dubzzz/fast-check/blob/main/CONTRIBUTING.md --> **Advanced** <!-- How to fill the advanced section is detailed below! --> - [x] Category: ✨ Introduce new feature - [x] Impacts: N/A --- Open questions: 1. Is this really the best place/way to implement this? (see my comments on #6039) 2. See `// TODO: Do we need to clone here?` in the PR code. If I _do_ need to clone, then what should I be using to do that? Answer: I think we don't need cloning here as long as we implement shrinking correctly 3. I have not been able to figure out how to implement the logic that throws if we accidentally don't replace some `placeholderSymbol`. I tried tracking them in a local set from `nat().map(...)` and then deleting them later when we replace, and asserting we have zero after, but this was causing problems, so I'm probably missing something. This seems like a critical feature in order to ship this 4. Neither `assertProduceValuesShrinkableWithoutContext` nor `assertShrinkProducesSameValueWithoutInitialContext` pass for `circular: true`. Does that make sense? Should they pass? If so, then what am I doing wrong? --------- Co-authored-by: Nicolas DUBIEN <[email protected]>
1 parent 4baddd8 commit e4b8d29

File tree

3 files changed

+426
-56
lines changed

3 files changed

+426
-56
lines changed

packages/fast-check/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,19 @@ export function resolveSize(size: Exclude<SizeForArbitrary, 'max'> | undefined):
192192
}
193193
return relativeSizeToSize(size, defaultSize);
194194
}
195+
196+
/** @internal */
197+
export function invertSize(size: Size): Size {
198+
switch (size) {
199+
case 'xsmall':
200+
return 'xlarge';
201+
case 'small':
202+
return 'large';
203+
case 'medium':
204+
return 'medium';
205+
case 'large':
206+
return 'small';
207+
case 'xlarge':
208+
return 'xsmall';
209+
}
210+
}

packages/fast-check/src/arbitrary/letrec.ts

Lines changed: 168 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { LazyArbitrary } from './_internals/LazyArbitrary';
22
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
3-
import { safeHasOwnProperty } from '../utils/globals';
3+
import { safeAdd, safeHas, safeHasOwnProperty } from '../utils/globals';
4+
import { invertSize, resolveSize, type SizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';
5+
import { nat } from './nat.js';
6+
import { record } from './record.js';
7+
import { array } from './array.js';
8+
import { noShrink } from './noShrink.js';
49

10+
const safeArrayIsArray = Array.isArray;
511
const safeObjectCreate = Object.create;
12+
const safeObjectEntries = Object.entries;
613

714
/**
815
* Type of the value produced by {@link letrec}
@@ -50,6 +57,154 @@ export type LetrecLooselyTypedTie = (key: string) => Arbitrary<unknown>;
5057
*/
5158
export type LetrecLooselyTypedBuilder<T> = (tie: LetrecLooselyTypedTie) => LetrecValue<T>;
5259

60+
/**
61+
* Constraints to be applied on {@link letrec}
62+
* @remarks Since 4.4.0
63+
* @public
64+
*/
65+
export interface LetrecConstraints {
66+
/**
67+
* Generate objects with circular references
68+
* @defaultValue false
69+
* @remarks Since 4.4.0
70+
*/
71+
withCycles?: boolean | CycleConstraints;
72+
}
73+
74+
/**
75+
* Constraints to be applied on {@link LetrecConstraints.withCycles}
76+
* @remarks Since 4.4.0
77+
* @public
78+
*/
79+
export interface CycleConstraints {
80+
/**
81+
* Define how frequently cycles should occur in the generated values (at max)
82+
* @remarks Since 4.4.0
83+
*/
84+
frequencySize?: Exclude<SizeForArbitrary, 'max'>;
85+
}
86+
87+
/** @internal */
88+
function letrecWithoutCycles<T>(builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>): LetrecValue<T> {
89+
const lazyArbs: { [K in keyof T]?: LazyArbitrary<unknown> } = safeObjectCreate(null);
90+
const tie = (key: keyof T): Arbitrary<any> => {
91+
if (!safeHasOwnProperty(lazyArbs, key)) {
92+
// Call to hasOwnProperty ensures that the property key will be defined
93+
lazyArbs[key] = new LazyArbitrary(String(key));
94+
}
95+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
96+
return lazyArbs[key]!;
97+
};
98+
const strictArbs = builder(tie as any);
99+
for (const key in strictArbs) {
100+
if (!safeHasOwnProperty(strictArbs, key)) {
101+
// Prevents accidental iteration over properties inherited from an object’s prototype
102+
continue;
103+
}
104+
const lazyAtKey: LazyArbitrary<unknown> | undefined = lazyArbs[key];
105+
const lazyArb = lazyAtKey !== undefined ? lazyAtKey : new LazyArbitrary(key);
106+
lazyArb.underlying = strictArbs[key];
107+
lazyArbs[key] = lazyArb;
108+
}
109+
return strictArbs;
110+
}
111+
112+
/** @internal */
113+
export function derefPools<T>(pools: { [K in keyof T]: unknown[] }, placeholderSymbol: symbol): void {
114+
const visited = new Set();
115+
function deref(value: unknown, source?: Record<PropertyKey, unknown>, sourceKey?: PropertyKey) {
116+
if (typeof value !== 'object' || value === null) {
117+
return;
118+
}
119+
120+
if (safeHas(visited, value)) {
121+
return;
122+
}
123+
safeAdd(visited, value);
124+
125+
if (safeHasOwnProperty(value, placeholderSymbol)) {
126+
// This is a while loop because it's possible for an arbitrary to be defined as just `arb: tie('otherArb')`, in
127+
// which case what the `arb` generates is also a placeholder.
128+
let currentValue: unknown = value;
129+
do {
130+
const { key, index } = (currentValue as { [placeholderSymbol]: { key: keyof T; index: number } })[
131+
placeholderSymbol
132+
];
133+
const pool = pools[key];
134+
currentValue = pool[index % pool.length];
135+
if (source !== undefined && sourceKey !== undefined) {
136+
source[sourceKey] = currentValue;
137+
}
138+
} while (safeHasOwnProperty(currentValue, placeholderSymbol));
139+
return;
140+
}
141+
142+
if (safeArrayIsArray(value)) {
143+
for (let i = 0; i < value.length; i++) {
144+
deref(value[i], value as unknown as Record<PropertyKey, unknown>, i);
145+
}
146+
} else {
147+
for (const [key, item] of safeObjectEntries(value)) {
148+
deref(item, value as Record<PropertyKey, unknown>, key);
149+
}
150+
}
151+
}
152+
deref(pools);
153+
}
154+
155+
/** @internal */
156+
function letrecWithCycles<T>(
157+
builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>,
158+
constraints: CycleConstraints,
159+
): LetrecValue<T> {
160+
const lazyArbs: { [K in keyof T]?: LazyArbitrary<unknown> } = safeObjectCreate(null);
161+
const tie = (key: keyof T): Arbitrary<any> => {
162+
if (!safeHasOwnProperty(lazyArbs, key)) {
163+
// Call to hasOwnProperty ensures that the property key will be defined
164+
lazyArbs[key] = new LazyArbitrary(String(key));
165+
}
166+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
167+
return lazyArbs[key]!;
168+
};
169+
const strictArbs = builder(tie as any);
170+
171+
// Symbol to replace with a potentially circular reference later.
172+
const placeholderSymbol = Symbol('placeholder');
173+
const poolArbs: { [K in keyof T]: Arbitrary<unknown[]> } = safeObjectCreate(null);
174+
const poolConstraints = {
175+
minLength: 1,
176+
// Higher cycle frequency is achieved by using a smaller pool of objects, so we invert the input `frequency`.
177+
size: invertSize(resolveSize(constraints.frequencySize)),
178+
};
179+
for (const key in strictArbs) {
180+
if (!safeHasOwnProperty(strictArbs, key)) {
181+
// Prevents accidental iteration over properties inherited from an object’s prototype
182+
continue;
183+
}
184+
const lazyAtKey: LazyArbitrary<unknown> | undefined = lazyArbs[key];
185+
const lazyArb = lazyAtKey !== undefined ? lazyAtKey : new LazyArbitrary(key);
186+
lazyArb.underlying = noShrink(nat().map((index) => ({ [placeholderSymbol]: { key, index } })));
187+
lazyArbs[key] = lazyArb;
188+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
189+
poolArbs[key] = array(strictArbs[key]!, poolConstraints);
190+
}
191+
192+
for (const key in strictArbs) {
193+
if (!safeHasOwnProperty(strictArbs, key)) {
194+
// Prevents accidental iteration over properties inherited from an object’s prototype
195+
continue;
196+
}
197+
198+
const poolsArb = record(poolArbs as any) as Arbitrary<{ [K in keyof T]: unknown[] }>;
199+
strictArbs[key] = poolsArb.map((pools) => {
200+
derefPools(pools, placeholderSymbol);
201+
return pools[key][0];
202+
}) as (typeof strictArbs)[typeof key];
203+
}
204+
205+
return strictArbs;
206+
}
207+
53208
/**
54209
* For mutually recursive types
55210
*
@@ -72,7 +227,10 @@ export type LetrecLooselyTypedBuilder<T> = (tie: LetrecLooselyTypedTie) => Letre
72227
* @remarks Since 1.16.0
73228
* @public
74229
*/
75-
export function letrec<T>(builder: T extends Record<string, unknown> ? LetrecTypedBuilder<T> : never): LetrecValue<T>;
230+
export function letrec<T>(
231+
builder: T extends Record<string, unknown> ? LetrecTypedBuilder<T> : never,
232+
constraints?: LetrecConstraints,
233+
): LetrecValue<T>;
76234
/**
77235
* For mutually recursive types
78236
*
@@ -92,27 +250,12 @@ export function letrec<T>(builder: T extends Record<string, unknown> ? LetrecTyp
92250
* @remarks Since 1.16.0
93251
* @public
94252
*/
95-
export function letrec<T>(builder: LetrecLooselyTypedBuilder<T>): LetrecValue<T>;
96-
export function letrec<T>(builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>): LetrecValue<T> {
97-
const lazyArbs: { [K in keyof T]?: LazyArbitrary<unknown> } = safeObjectCreate(null);
98-
const tie = (key: keyof T): Arbitrary<any> => {
99-
if (!safeHasOwnProperty(lazyArbs, key)) {
100-
// Call to hasOwnProperty ensures that the property key will be defined
101-
lazyArbs[key] = new LazyArbitrary(String(key));
102-
}
103-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
104-
return lazyArbs[key]!;
105-
};
106-
const strictArbs = builder(tie as any);
107-
for (const key in strictArbs) {
108-
if (!safeHasOwnProperty(strictArbs, key)) {
109-
// Prevents accidental iteration over properties inherited from an object’s prototype
110-
continue;
111-
}
112-
const lazyAtKey: LazyArbitrary<unknown> | undefined = lazyArbs[key];
113-
const lazyArb = lazyAtKey !== undefined ? lazyAtKey : new LazyArbitrary(key);
114-
lazyArb.underlying = strictArbs[key];
115-
lazyArbs[key] = lazyArb;
116-
}
117-
return strictArbs;
253+
export function letrec<T>(builder: LetrecLooselyTypedBuilder<T>, constraints?: LetrecConstraints): LetrecValue<T>;
254+
export function letrec<T>(
255+
builder: LetrecLooselyTypedBuilder<T> | LetrecTypedBuilder<T>,
256+
constraints: LetrecConstraints = {},
257+
): LetrecValue<T> {
258+
return constraints.withCycles
259+
? letrecWithCycles(builder, constraints.withCycles === true ? {} : constraints.withCycles)
260+
: letrecWithoutCycles(builder);
118261
}

0 commit comments

Comments
 (0)