-
-
Notifications
You must be signed in to change notification settings - Fork 200
✨ Add circular option to fc.letrec
#6040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b797107
b924c68
1568369
6231a35
f7cdd37
a766dff
6b98ec7
d57b84c
7d0427b
224ba73
bc21ce1
c2e0ad9
25d6ea6
1cda443
2f7caab
9a4d162
85bc340
8e81a17
0069116
a0763ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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} | ||
|
|
@@ -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.4.0 | ||
| * @public | ||
| */ | ||
| export interface LetrecConstraints { | ||
| /** | ||
| * Generate objects with circular references | ||
| * @defaultValue false | ||
| * @remarks Since 4.4.0 | ||
| */ | ||
| withCycles?: boolean | CycleConstraints; | ||
| } | ||
|
|
||
| /** | ||
| * Constraints to be applied on {@link LetrecConstraints.withCycles} | ||
| * @remarks Since 4.4.0 | ||
| * @public | ||
| */ | ||
| export interface CycleConstraints { | ||
| /** | ||
| * Define how frequently cycles should occur in the generated values (at max) | ||
| * @remarks Since 4.4.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); | ||
TomerAberbach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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 } }))); | ||
TomerAberbach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| lazyArbs[key] = lazyArb; | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| poolArbs[key] = array(strictArbs[key]!, poolConstraints); | ||
TomerAberbach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
+179
to
+190
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
TomerAberbach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| derefPools(pools, placeholderSymbol); | ||
| return pools[key][0]; | ||
| }) as (typeof strictArbs)[typeof key]; | ||
| } | ||
|
|
||
| return strictArbs; | ||
| } | ||
|
|
||
| /** | ||
| * For mutually recursive types | ||
| * | ||
|
|
@@ -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 | ||
| * | ||
|
|
@@ -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); | ||
| } | ||
There was a problem hiding this comment.
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).