import * as Cause from "../../Cause.ts" import * as Effect from "../../Effect.ts" import * as Option from "../../Option.ts" import * as Result from "../../Result.ts" import * as InternalArray from "../array.ts" import { done } from "../core.ts" /** @internal */ export type ShrinkPull = Effect.Effect /** * Builds a cause handler that recovers typed failures, or propagates only * defects and interruptions, preserving their original annotations. * * @internal */ export const onTypedFailure = ( f: (error: E) => Effect.Effect ) => (cause: Cause.Cause): Effect.Effect => { if (!cause.reasons.every(Cause.isFailReason)) { return Effect.failCause(Cause.fromReasons(cause.reasons.filter( (reason): reason is Cause.Die | Cause.Interrupt => reason._tag !== "Fail" ))) } const error = Cause.findError(cause) return Result.isFailure(error) ? Effect.failCause(error.failure) : f(error.success) } /** @internal */ export interface Sample { readonly _tag: "Generated" readonly value: A readonly shrinks: ShrinkPull> | undefined } /** @internal */ export interface Discarded { readonly _tag: "Discarded" } /** @internal */ export type Attempt = Sample | Discarded /** @internal */ export type Computation = A | Effect.Effect /** @internal */ export type Generation = Computation> /** @internal */ export interface GenerationRandom { readonly nextUint32: () => number readonly nextDoubleUnsafe: () => number readonly clone: () => GenerationRandom readonly copyFrom: (source: GenerationRandom) => void readonly snapshot: () => readonly [number, number, number, number] } /** @internal */ export interface GenerationState { readonly size: number readonly shrinks: boolean readonly biasFactor: number readonly nullPrototype: boolean readonly random: GenerationRandom readonly budget: { remaining: number } } /** @internal */ export interface Generator { readonly minCost: number readonly generate: (state: GenerationState) => Generation } /** @internal */ export interface Compiled extends Generator { minCost: number recursive: boolean mayRecurse: boolean dependencies: ReadonlyArray> computeMinCost: () => number generate: (state: GenerationState) => Generation } /** @internal */ export const discarded: Discarded = { _tag: "Discarded" } /** @internal */ export function mapComputation(self: Computation, f: (value: A) => B): Computation { return Effect.isEffect(self) ? Effect.mapEager(self as Effect.Effect, f) : f(self as A) } /** @internal */ export function flatMapComputation( self: Computation, f: (value: A) => Computation ): Computation { return Effect.isEffect(self) ? Effect.flatMapEager(self as Effect.Effect, (value) => toEffect(f(value))) : f(self as A) } /** @internal */ export function toEffect(self: Computation): Effect.Effect { return Effect.isEffect(self) ? self as Effect.Effect : Effect.succeed(self as A) } /** @internal */ export function isAttempt(self: Generation): self is Attempt { return (self as Attempt)._tag === "Generated" || (self as Attempt)._tag === "Discarded" } /** @internal */ export function mapGeneration( self: Generation, f: (value: Attempt) => Attempt ): Generation { return isAttempt(self) ? f(self) : Effect.mapEager(self, f) } /** @internal */ export function flatMapGeneration( self: Generation, f: (value: Attempt) => Generation ): Generation { return isAttempt(self) ? f(self) : Effect.flatMapEager(self, (value) => toEffectGeneration(f(value))) } /** @internal */ export function toEffectGeneration(self: Generation): Effect.Effect> { return isAttempt(self) ? Effect.succeed(self) : self } /** @internal */ export function pullFromArray(values: ReadonlyArray): ShrinkPull { let index = 0 return Effect.suspend(() => index >= values.length ? done() : Effect.succeed(values[index++])) } /** @internal */ export function concatPulls(pulls: ReadonlyArray>): ShrinkPull { let index = 0 const loop = (): ShrinkPull => Effect.suspend(() => index >= pulls.length ? done() : Effect.catchCause( pulls[index], onTypedFailure(() => { index++ return loop() }) ) ) return loop() } /** @internal */ export const makeSample = (value: A, shrinks?: ShrinkPull>): Sample => ({ _tag: "Generated", value, shrinks }) /** @internal */ export function makeSampleWithLazyShrinks( value: A, makeShrinks: () => ShrinkPull> | undefined ): Sample { let shrinks: ShrinkPull> | undefined return makeSample( value, Effect.suspend(() => { shrinks ??= makeShrinks() ?? done() return shrinks }) ) } /** @internal */ export interface Retained { readonly _tag: "Retained" readonly value: A readonly shrinks: (() => ShrinkPull | Discarded>) | undefined } // Collection candidates share their children. Give each candidate its own cursor // over a lazy child history so exploring one branch cannot exhaust another. /** @internal */ export function retain(sample: Sample): Retained { const source = sample.shrinks if (source === undefined) return { _tag: "Retained", value: sample.value, shrinks: undefined } let history: Array | Discarded> | undefined let ended = false return { _tag: "Retained", value: sample.value, shrinks: () => { let index = 0 return Effect.suspend(() => { if (history !== undefined && index < history.length) return Effect.succeed(history[index++]) if (ended) return done() return Effect.matchCauseEffect(source, { onFailure: onTypedFailure(() => { ended = true return done() }), onSuccess: (attempt) => { const item = attempt._tag === "Discarded" ? attempt : retain(attempt) history ??= [] history.push(item) index++ return Effect.succeed(item) } }) }) } } } /** @internal */ export function fromRetained(self: Retained): Sample { return makeSample( self.value, self.shrinks === undefined ? undefined : Effect.map(self.shrinks(), (attempt) => attempt._tag === "Discarded" ? attempt : fromRetained(attempt)) ) } /** @internal */ export function mapAttempt(self: Attempt, f: (sample: Sample) => Sample): Attempt { return self._tag === "Discarded" ? self : f(self) } /** @internal */ export function sampleFromShrink(value: A, shrink: (value: A) => ReadonlyArray): Sample { const values = shrink(value) return makeSample( value, values.length === 0 ? undefined : Effect.map(pullFromArray(values), (value) => sampleFromShrink(value, shrink)) ) } /** @internal */ export function sampleFromValidatedShrink( value: A, shrink: (value: A) => ReadonlyArray, validate: (value: A) => Computation> ): Sample { return makeSampleWithLazyShrinks( value, () => Effect.flatMapEager( pullFromArray(shrink(value)), (candidate) => Effect.mapEager(toEffect(validate(candidate)), (validated) => Option.isSome(validated) ? sampleFromValidatedShrink(validated.value, shrink, validate) : discarded) ) ) } /** @internal */ export function mapSample(self: Sample, f: (value: A) => B): Sample { return makeSample( f(self.value), self.shrinks === undefined ? undefined : Effect.map(self.shrinks, (attempt) => mapAttempt(attempt, (sample) => mapSample(sample, f))) ) } /** @internal */ export function productSample( children: ReadonlyArray>, make: (children: ReadonlyArray<{ readonly value: any }>) => A ): Sample { const value = make(children) if (!children.some((child) => child.shrinks !== undefined)) return makeSample(value) const rebuild = (children: ReadonlyArray>, start = 0): Sample => { const pulls: Array>> = [] // Preserve field order explicitly instead of relying on exhausted sibling cursors. for (let index = start; index < children.length; index++) { const shrinks = children[index].shrinks if (shrinks !== undefined) { pulls.push(Effect.map( shrinks(), (attempt) => attempt._tag === "Discarded" ? attempt : rebuild(InternalArray.replaceAt(children, index, attempt), index) )) } } return makeSample(make(children), pulls.length === 0 ? undefined : concatPulls(pulls)) } return makeSampleWithLazyShrinks(value, () => rebuild(children.map(retain)).shrinks) } function filterMapPull( source: ShrinkPull>, f: (value: A) => Computation> ): ShrinkPull> { const stack: Array>> = [source] const loop = (): ShrinkPull> => Effect.suspend(() => { const current = stack[stack.length - 1] if (current === undefined) return done() return Effect.matchCauseEffect(current, { onFailure: onTypedFailure(() => { stack.pop() return loop() }), onSuccess: (attempt) => { if (attempt._tag === "Discarded") return Effect.succeed>(attempt) const sample = attempt return Effect.flatMapEager(toEffect(f(sample.value)), (mapped) => { if (Option.isSome(mapped)) { return Effect.succeed>(makeSample( mapped.value, sample.shrinks === undefined ? undefined : filterMapPull(sample.shrinks, f) )) } if (sample.shrinks !== undefined) stack.push(sample.shrinks) return Effect.succeed>(discarded) }) } }) }) return loop() } function filterPull(source: ShrinkPull>, predicate: (value: A) => boolean): ShrinkPull> { const stack: Array>> = [source] const loop = (): ShrinkPull> => Effect.suspend(() => { const current = stack[stack.length - 1] if (current === undefined) return done() return Effect.matchCauseEffect(current, { onFailure: onTypedFailure(() => { stack.pop() return loop() }), onSuccess: (attempt) => { if (attempt._tag === "Discarded") return Effect.succeed>(attempt) const sample = attempt if (predicate(sample.value)) { return Effect.succeed>(makeSample( sample.value, sample.shrinks === undefined ? undefined : filterPull(sample.shrinks, predicate) )) } if (sample.shrinks !== undefined) stack.push(sample.shrinks) return Effect.succeed>(discarded) } }) }) return loop() } /** @internal */ export const filterMapSample = ( self: Sample, f: (value: A) => Computation> ): Computation | undefined> => mapComputation( f(self.value), (value) => Option.isNone(value) ? undefined : makeSample( value.value, self.shrinks === undefined ? undefined : filterMapPull(self.shrinks, f) ) ) /** @internal */ export const filterMapGeneration = ( self: Generation, f: (value: A) => Computation> ): Generation => flatMapGeneration(self, (attempt) => attempt._tag === "Discarded" ? attempt : mapComputation( filterMapSample(attempt, f), (sample) => sample ?? discarded )) /** @internal */ export const filterSample = (self: Sample, predicate: (value: A) => boolean): Sample | undefined => predicate(self.value) ? self.shrinks === undefined ? self : makeSample(self.value, filterPull(self.shrinks, predicate)) : undefined /** @internal */ export function makeGenerator(minCost: number, generate: Generator["generate"]): Generator { return { minCost, generate } } /** @internal */ export const generateWithReservedBudget = ( child: Generator, state: GenerationState, reserved: number ): Generation => { if (child.minCost + reserved > state.budget.remaining) return discarded if (reserved === 0) return child.generate(state) state.budget.remaining -= reserved return mapGeneration(child.generate(state), (attempt) => { state.budget.remaining += reserved return attempt }) } /** @internal */ export function generateProduct( children: ReadonlyArray>, state: GenerationState, order: ReadonlyArray | undefined, initialReserved: number ): Computation> | undefined> { let reserved = initialReserved if (reserved > state.budget.remaining) return undefined const out = new Array>(children.length) let index = 0 const loop = (): Computation> | undefined> => { while (index < children.length) { const childIndex = order?.[index] ?? index index++ const child = children[childIndex] reserved -= child.minCost const generated = generateWithReservedBudget(child, state, reserved) if (isAttempt(generated)) { if (generated._tag === "Discarded") return undefined out[childIndex] = generated continue } return Effect.flatMapEager(generated, (attempt) => { if (attempt._tag === "Discarded") return Effect.succeed(undefined) out[childIndex] = attempt return toEffect(loop()) }) } return out } return loop() } /** @internal */ export function makeCompiled( dependencies: ReadonlyArray>, computeMinCost: () => number, generate: Compiled["generate"] ): Compiled { return { minCost: Number.POSITIVE_INFINITY, recursive: false, mayRecurse: false, dependencies, computeMinCost, generate } } /** @internal */ export const randomIndex = (state: GenerationState, length: number): number => length === 1 ? 0 : randomUint32Below(state, length) /** @internal */ export function generateUnion(members: ReadonlyArray>, state: GenerationState): Generation { const eligible = members.filter((member) => member.minCost <= state.budget.remaining) if (eligible.length === 0) return discarded const selected = eligible[randomIndex(state, eligible.length)] return mapGeneration(selected.generate(state), (attempt) => { if (!state.shrinks || attempt._tag === "Discarded") return attempt let fallback = members[0] for (let index = 1; index < members.length; index++) { if (members[index].minCost < fallback.minCost) fallback = members[index] } if (fallback.minCost >= selected.minCost) return attempt const fallbackRandom = state.random.clone() // This lazy cross-branch fallback follows fast-check v4.9.0's FrequencyArbitrary withCrossShrink idea (MIT): a // value selected from a recursive branch first shrinks toward the productive base branch. // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/FrequencyArbitrary.ts let pulled = false const fallbackPull = Effect.suspend(() => { if (pulled) return done() pulled = true return toEffectGeneration(fallback.generate({ ...state, random: fallbackRandom, budget: { remaining: fallback.minCost } })) }) return makeSample( attempt.value, attempt.shrinks === undefined ? fallbackPull : concatPulls([fallbackPull, attempt.shrinks]) ) }) } const numberOfUint32Values = 0x100000000 const numberOfDoubleValues = 0x20000000000000 function randomUint32Below(state: GenerationState, rangeSize: number): number { // Equal-size buckets plus rejection apply the same unbiased selection principle as pure-rand v8.4.1's // uniformIntInternal (MIT), used by fast-check v4.9.0. Small discrete ranges consume one PRNG word instead of // constructing a 53-bit double. // https://github.com/dubzzz/pure-rand/blob/v8.4.1/src/distribution/uniformInt.ts const bucketSize = Math.floor(numberOfUint32Values / rangeSize) const maximumAccepted = bucketSize * rangeSize let value = state.random.nextUint32() while (value >= maximumAccepted) value = state.random.nextUint32() return Math.floor(value / bucketSize) } function makeRandomUint32Below(rangeSize: number): (state: GenerationState) => number { const bucketSize = Math.floor(numberOfUint32Values / rangeSize) const maximumAccepted = bucketSize * rangeSize return (state) => { let value = state.random.nextUint32() while (value >= maximumAccepted) value = state.random.nextUint32() return Math.floor(value / bucketSize) } } /** @internal */ export const randomInt = (state: GenerationState, minimum: number, maximum: number): number => { minimum = Math.ceil(minimum) maximum = Math.floor(maximum) if (minimum === maximum) return minimum const width = maximum - minimum + 1 if (width <= numberOfUint32Values) return minimum + randomUint32Below(state, width) if (width <= numberOfDoubleValues) { // Equal-size buckets plus rejection apply the same unbiased selection principle as pure-rand v8.4.1's uniformInt // (MIT), used by fast-check v4.9.0. // https://github.com/dubzzz/pure-rand/blob/v8.4.1/src/distribution/uniformInt.ts const bucketSize = Math.floor(numberOfDoubleValues / width) const maximumAccepted = bucketSize * width while (true) { const value = Math.floor(state.random.nextDoubleUnsafe() * numberOfDoubleValues) if (value < maximumAccepted) return minimum + Math.floor(value / bucketSize) } } return Number(randomBigInt(state, BigInt(minimum), BigInt(maximum))) } /** @internal */ export const makeObject = (nullPrototype: boolean): Record => nullPrototype ? Object.create(null) : {} /** @internal */ export const randomLength = (state: GenerationState, minimum: number, maximum: number): number => { if (minimum === maximum) return minimum // This applies fast-check v4.9.0's run-dependent numeric edge-bias principle to the small discrete length domain // (MIT): occasionally target either boundary, otherwise retain uniform selection across the complete interval. // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/IntegerArbitrary.ts if (randomInt(state, 1, state.biasFactor) === 1) return randomBoolean(state) ? minimum : maximum return randomInt(state, minimum, maximum) } /** @internal */ export const randomBigInt = (state: GenerationState, minimum: bigint, maximum: bigint): bigint => { if (minimum === maximum) return minimum const width = maximum - minimum + BigInt(1) const bitLength = (width - BigInt(1)).toString(2).length const leadingBits = (bitLength - 1) % 32 + 1 const leadingRange = 2 ** leadingBits // Arbitrary-width rejection sampling follows the same principle as pure-rand v8.4.1's uniformBigInt (MIT), used by // fast-check v4.9.0. This implementation draws unsigned words from the private attempt PRNG instead of pure-rand. // https://github.com/dubzzz/pure-rand/blob/v8.4.1/src/distribution/uniformBigInt.ts while (true) { let value = BigInt(randomUint32Below(state, leadingRange)) for (let remaining = bitLength - leadingBits; remaining > 0; remaining -= 32) { const word = BigInt(state.random.nextUint32()) value = value << BigInt(32) | word } if (value < width) return minimum + value } } function makeRandomInt(minimum: number, maximum: number): (state: GenerationState) => number { minimum = Math.ceil(minimum) maximum = Math.floor(maximum) if (minimum === maximum) return () => minimum const width = maximum - minimum + 1 if (width <= numberOfUint32Values) { const randomBelow = makeRandomUint32Below(width) return (state) => minimum + randomBelow(state) } if (width <= numberOfDoubleValues) { const bucketSize = Math.floor(numberOfDoubleValues / width) const maximumAccepted = bucketSize * width return (state) => { while (true) { const value = Math.floor(state.random.nextDoubleUnsafe() * numberOfDoubleValues) if (value < maximumAccepted) return minimum + Math.floor(value / bucketSize) } } } const random = makeRandomBigInt(BigInt(minimum), BigInt(maximum)) return (state) => Number(random(state)) } function makeRandomBigInt(minimum: bigint, maximum: bigint): (state: GenerationState) => bigint { if (minimum === maximum) return () => minimum const width = maximum - minimum + BigInt(1) const bitLength = (width - BigInt(1)).toString(2).length const leadingBits = (bitLength - 1) % 32 + 1 const leadingRange = 2 ** leadingBits const randomLeading = makeRandomUint32Below(leadingRange) return (state) => { while (true) { let value = BigInt(randomLeading(state)) for (let remaining = bitLength - leadingBits; remaining > 0; remaining -= 32) { value = value << BigInt(32) | BigInt(state.random.nextUint32()) } if (value < width) return minimum + value } } } interface NumberRange { readonly minimum: number readonly maximum: number } interface BigIntRange { readonly minimum: bigint readonly maximum: bigint } // The numeric edge ranges and their 2:1 preference for the edge closest to zero follow fast-check v4.9.0's // BiasNumericRange, IntegerArbitrary, and BigIntArbitrary (MIT). The policy stays private to the Arbitrary engine. // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/helpers/BiasNumericRange.ts // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/IntegerArbitrary.ts // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/BigIntArbitrary.ts function numberBiasRanges(minimum: number, maximum: number): ReadonlyArray { if (minimum === maximum) return [{ minimum, maximum }] if (minimum < 0 && maximum > 0) { const low = Math.floor(Math.log2(-minimum)) const high = Math.floor(Math.log2(maximum)) return [ { minimum: -low, maximum: high }, { minimum: maximum - high, maximum }, { minimum, maximum: minimum + low } ] } const gap = Math.floor(Math.log2(maximum - minimum)) const closeToMinimum = { minimum, maximum: minimum + gap } const closeToMaximum = { minimum: maximum - gap, maximum } return minimum < 0 ? [closeToMaximum, closeToMinimum] : [closeToMinimum, closeToMaximum] } function bigIntLogLike(value: bigint): bigint { return value === BigInt(0) ? BigInt(0) : BigInt(value.toString().length) } function bigIntBiasRanges(minimum: bigint, maximum: bigint): ReadonlyArray { if (minimum === maximum) return [{ minimum, maximum }] if (minimum < BigInt(0) && maximum > BigInt(0)) { const low = bigIntLogLike(-minimum) const high = bigIntLogLike(maximum) return [ { minimum: -low, maximum: high }, { minimum: maximum - high, maximum }, { minimum, maximum: minimum + low } ] } const gap = bigIntLogLike(maximum - minimum) const closeToMinimum = { minimum, maximum: minimum + gap } const closeToMaximum = { minimum: maximum - gap, maximum } return minimum < BigInt(0) ? [closeToMaximum, closeToMinimum] : [closeToMinimum, closeToMaximum] } function selectBiased(state: GenerationState, values: ReadonlyArray): A { if (values.length === 1) return values[0] const index = randomInt(state, -2 * (values.length - 1), values.length - 2) return index < 0 ? values[0] : values[index + 1] } /** @internal */ export function makeRandomNumericInt( minimum: number, maximum: number ): (state: GenerationState) => number { const full = makeRandomInt(minimum, maximum) const biased = numberBiasRanges(minimum, maximum).map((range) => makeRandomInt(range.minimum, range.maximum)) return (state) => { const random = randomInt(state, 1, state.biasFactor) === 1 ? selectBiased(state, biased) : full return random(state) } } /** @internal */ export function makeRandomNumericBigInt( minimum: bigint, maximum: bigint ): (state: GenerationState) => bigint { const full = makeRandomBigInt(minimum, maximum) const biased = bigIntBiasRanges(minimum, maximum).map((range) => makeRandomBigInt(range.minimum, range.maximum)) return (state) => { const random = randomInt(state, 1, state.biasFactor) === 1 ? selectBiased(state, biased) : full return random(state) } } // The monotone IEEE-754 index and adjacent-number navigation follow the model used by fast-check v4.9.0's // DoubleHelpers (MIT). This implementation uses a direct 64-bit bit cast instead of its exponent decomposition. // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/helpers/DoubleHelpers.ts const numberBuffer = new ArrayBuffer(8) const numberView = new DataView(numberBuffer) const numberSignMask = BigInt(1) << BigInt(63) const numberBitsMask = (BigInt(1) << BigInt(64)) - BigInt(1) /** @internal */ export function numberToIndex(value: number): bigint { numberView.setFloat64(0, value) const bits = numberView.getBigUint64(0) const unsigned = (bits & numberSignMask) === BigInt(0) ? bits | numberSignMask : ~bits & numberBitsMask return unsigned - numberSignMask } /** @internal */ export function indexToNumber(index: bigint): number { const unsigned = index + numberSignMask const bits = (unsigned & numberSignMask) === BigInt(0) ? ~unsigned & numberBitsMask : unsigned ^ numberSignMask numberView.setBigUint64(0, bits) return numberView.getFloat64(0) } /** @internal */ export function nextNumber(value: number): number { if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) return value if (value === 0) return Number.MIN_VALUE return indexToNumber(numberToIndex(value) + BigInt(1)) } /** @internal */ export function previousNumber(value: number): number { if (Number.isNaN(value) || value === Number.NEGATIVE_INFINITY) return value if (value === 0) return -Number.MIN_VALUE return indexToNumber(numberToIndex(value) - BigInt(1)) } /** @internal */ export function makeRandomNumber( minimum: number, maximum: number, allowNaN: boolean ): (state: GenerationState) => number { const minimumIndex = numberToIndex(minimum) const maximumIndex = numberToIndex(maximum) const nanBelow = allowNaN && maximumIndex <= BigInt(0) const randomIndex = makeRandomNumericBigInt( nanBelow ? minimumIndex - BigInt(1) : minimumIndex, allowNaN && !nanBelow ? maximumIndex + BigInt(1) : maximumIndex ) return (state) => { const index = randomIndex(state) return index < minimumIndex || index > maximumIndex ? Number.NaN : indexToNumber(index) } } /** @internal */ export const randomBoolean = (state: GenerationState): boolean => (state.random.nextUint32() & 1) === 1 /** @internal */ export function shuffle(state: GenerationState, elements: Iterable): Array { const buffer = Array.from(elements) for (let index = buffer.length - 1; index >= 1; index--) { const target = randomUint32Below(state, index + 1) const value = buffer[index]! buffer[index] = buffer[target]! buffer[target] = value } return buffer }