import { interval } from './generator/integer' import { Random } from './Random' import { Shrinkable } from './Shrinkable' import { shrinkArrayLength } from './shrinker/array' import { Stream } from './Stream' /** * Defines the core interface for generating random values along with their shrinkable counterparts. * Shrinkable values are essential for property-based testing, allowing the system to find the smallest failing example. * @template T The type of value to generate. */ export interface Generator { /** * Generates a random value wrapped in a Shrinkable container. * @param rand The random number generator instance. * @returns A Shrinkable containing the generated value and its potential smaller versions. */ generate(rand: Random): Shrinkable /** * Transforms the generated values using a provided function. * @template U The type of the transformed value. * @param transformer A function to apply to the generated value. * @returns A new Generator producing transformed values. */ map(transformer: (arg: T) => U): Generator /** * Chains the generation process by using the output of this generator to create a new generator. * This is useful for creating dependent random values. * @template U The type produced by the subsequent generator. * @param genFactory A function that takes the generated value and returns a new Generator. * @returns A new Generator producing values from the chained generator. */ flatMap(genFactory: (arg: T) => Generator): Generator /** * Similar to flatMap, but preserves the original value, returning a tuple. * @template U The type produced by the subsequent generator. * @param genFactory A function that takes the generated value and returns a new Generator. * @returns A new Generator producing tuples of [originalValue, newValue]. */ chain(genFactory: (arg: T) => Generator): Generator<[T, U]> /** * Chains generation assuming the current generator produces tuples, appending the new value. * Requires the current generator to produce an array/tuple. * @template Ts The tuple type produced by the current generator. * @template U The type produced by the subsequent generator. * @param genFactory A function that takes the generated tuple and returns a new Generator. * @returns A new Generator producing tuples with the new value appended. */ chainAsTuple(genFactory: (arg: Ts) => Generator): Generator<[...Ts, U]> /** * Repeatedly applies a generator factory to the last generated value a specified number of times. * The final value is returned. * @param genFactory A function that takes the last value and returns the next generator. * @param minSize The minimum number of aggregation steps. * @param maxSize The maximum number of aggregation steps. * @returns A new Generator producing the aggregated value. */ aggregate(genFactory: (arg: T) => Generator, minSize: number, maxSize: number): Generator /** * Similar to aggregate, but collects all intermediate values into an array. * @param genFactory A function that takes the last value and returns the next generator. * @param minSize The minimum number of accumulation steps. * @param maxSize The maximum number of accumulation steps. * @returns A new Generator producing an array of accumulated values. */ accumulate(genFactory: (arg: T) => Generator, minSize: number, maxSize: number): Generator> /** * Filters the generated values based on a predicate. * It will keep generating values until one satisfies the predicate. * @param filterer A function that returns true if the value should be kept. * @returns A new Generator producing only values that satisfy the predicate. */ filter(filterer: (value: T) => boolean): Generator } /** * A concrete implementation of the Generator interface. * @template T The type of value to generate. */ export class Arbitrary implements Generator { /** * Creates an instance of Arbitrary. * @param genFunction The core function used to generate Shrinkable values. */ constructor(readonly genFunction: GenFunction) {} generate(rand: Random): Shrinkable { return this.genFunction(rand) } map(transformer: (arg: T) => U): Generator { // Creates a new Arbitrary that applies the transformer to the generated Shrinkable's value. return new Arbitrary((rand: Random) => this.generate(rand).map(transformer)) } aggregate(genFactory: (arg: T) => Generator, minSize: number, maxSize: number): Generator { // Uses an integer generator to determine the number of aggregation steps. return interval(minSize, maxSize).flatMap( size => new Arbitrary((rand: Random) => { // Start with the initial value. let shr = this.generate(rand) // Apply the factory repeatedly. for (let i = 1; i < size; i++) shr = genFactory(shr.value).generate(rand) return shr }) ) } accumulate(genFactory: (arg: T) => Generator, minSize: number, maxSize: number): Generator> { return new Arbitrary>((rand: Random) => { const size = rand.interval(minSize, maxSize) if (size === 0) return new Shrinkable([]) // Handle empty accumulation. let shr = this.generate(rand) const shrArr = [shr] // Store all generated shrinkables. for (let i = 1; i < size; i++) { shr = genFactory(shr.value).generate(rand) shrArr.push(shr) } // Shrink the array primarily by length, then by shrinking the last element. // This is a common strategy for shrinking collections. return shrinkArrayLength(shrArr, minSize) .andThen(parent => { const shrArr = parent.value if (shrArr.length === 0) return new Stream() const lastElemShr = shrArr[shrArr.length - 1] const elemShrinks = lastElemShr.shrinks() if (elemShrinks.isEmpty()) return new Stream() // Create shrinks by replacing the last element with its own shrinks. return elemShrinks.transform(elem => { const copy = shrArr.concat() copy[copy.length - 1] = elem return new Shrinkable(copy) }) }) .map(arr => arr.map(shr => shr.value)) // Extract values from Shrinkables. }) } flatMap(genFactory: (arg: T) => Generator): Generator { return new Arbitrary((rand: Random) => { // Generate the initial value and map it to the *next* generator's Shrinkable. const intermediate: Shrinkable> = this .generate(rand) .map(value => genFactory(value).generate(rand)) // Shrinking involves two steps: // 1. Shrink the intermediate Shrinkable (which shrinks the *outer* value T). // 2. Shrink the inner Shrinkable (which shrinks the *inner* value U). return intermediate .andThen(interShr => // This flatMap combines shrinks from the inner Shrinkable. interShr.value.flatMap>(second => new Shrinkable(new Shrinkable(second))).shrinks() ) .map(shr => shr.value) // Extract the final U value. }) } chain(genFactory: (arg: T) => Generator): Generator<[T, U]> { return new Arbitrary<[T, U]>((rand: Random) => { // Similar to flatMap, but keeps the original value T. const intermediate: Shrinkable<[T, Shrinkable]> = this .generate(rand) .map(value => [value, genFactory(value).generate(rand)]) // Keep 'value' (type T). // Shrinking: // 1. Shrink the intermediate Shrinkable (shrinks T). // 2. Shrink the inner Shrinkable (shrinks U). return intermediate .andThen(interShr => // Combine shrinks from the inner Shrinkable. interShr.value[1] .flatMap<[T, Shrinkable]>( second => new Shrinkable([interShr.value[0], new Shrinkable(second)]) // Reconstruct the pair. ) .shrinks() ) .map(pair => [pair[0], pair[1].value]) // Extract the final U value from the inner Shrinkable. }) } chainAsTuple(genFactory: (arg: Ts) => Generator): Generator<[...Ts, U]> { return new Arbitrary<[...Ts, U]>((rand: Random) => { const intermediate: Shrinkable<[...Ts, Shrinkable]> = this.generate(rand).map(value => { // Expects the current generator to produce an array/tuple. if (!Array.isArray(value)) throw new Error('method unsupported for the type') const tuple = (value as unknown) as Ts // Append the new Shrinkable to the tuple. return [...tuple, genFactory(tuple).generate(rand)] }) // Shrinking logic similar to chain, adapted for tuples. return intermediate .andThen(interShr => { const head = interShr.value.slice(0, interShr.value.length - 1) as Ts const tail = interShr.value[interShr.value.length - 1] as Shrinkable // Combine shrinks from the tail element (Shrinkable). return tail .flatMap<[...Ts, Shrinkable]>(second => new Shrinkable([...head, new Shrinkable(second)])) .shrinks() }) .map(pair => [ ...(pair.slice(0, pair.length - 1) as Ts), // Extract the final U value from the last element. (pair[pair.length - 1] as Shrinkable).value, ]) }) } filter(filterer: (value: T) => boolean): Generator { return new Arbitrary((rand: Random) => { // Keep generating until a value satisfies the filter. // Note: This can potentially loop infinitely if the filter is too restrictive. while (true) { const shr = this.generate(rand) if (filterer(shr.value)) return shr.filter(filterer) // Apply filter to shrinks as well. } }) } } /** * A Generator implementation specifically designed for container-like structures (e.g., arrays, strings), * incorporating default size constraints. * @template T The type of value the container holds or the container type itself. */ export class ArbiContainer implements Generator { /** Default minimum size for generated containers. */ public static defaultMinSize: number = 0 /** Default maximum size for generated containers. */ public static defaultMaxSize: number = 100 /** * Creates an instance of ArbiContainer. * @param genFunction The core function used to generate Shrinkable values. * @param minSize Minimum size constraint for the generated container. * @param maxSize Maximum size constraint for the generated container. */ constructor( readonly genFunction: GenFunction, public minSize: number = ArbiContainer.defaultMinSize, public maxSize: number = ArbiContainer.defaultMaxSize ) {} // Most methods delegate to the underlying genFunction but wrap the result // in a new ArbiContainer to preserve the size constraints. generate(rand: Random): Shrinkable { // Potentially use minSize/maxSize within the genFunction if needed, // although the standard implementation doesn't directly enforce it here. // Size enforcement often happens within specific container generator functions (like array, string). return this.genFunction(rand) } map(transformer: (arg: T) => U): Generator { // Preserve size constraints when mapping. return new ArbiContainer((rand: Random) => this.generate(rand).map(transformer), this.minSize, this.maxSize) } flatMap(genFactory: (arg: T) => Generator): Generator { // Preserve size constraints when flatMapping. return new ArbiContainer( (rand: Random) => { // Standard flatMap logic. const intermediate: Shrinkable> = this .generate(rand) .map(value => genFactory(value).generate(rand)) return intermediate .andThen(interShr => interShr.value .flatMap>(second => new Shrinkable(new Shrinkable(second))) .shrinks() ) .map(pair => pair.value) }, this.minSize, this.maxSize ) } chain(genFactory: (arg: T) => Generator): Generator<[T, U]> { // Preserve size constraints when chaining. return new ArbiContainer<[T, U]>( (rand: Random) => { // Standard chain logic. const intermediate: Shrinkable<[T, Shrinkable]> = this .generate(rand) .map(value => [value, genFactory(value).generate(rand)]) return intermediate .andThen(interShr => interShr.value[1] .flatMap<[T, Shrinkable]>( second => new Shrinkable([interShr.value[0], new Shrinkable(second)]) ) .shrinks() ) .map(pair => [pair[0], pair[1].value]) }, this.minSize, this.maxSize ) } // Note: chainAsTuple in ArbiContainer currently returns an Arbitrary, not preserving ArbiContainer properties. // This might be intentional or an oversight depending on desired behavior. chainAsTuple(genFactory: (arg: Ts) => Generator): Generator<[...Ts, U]> { return new Arbitrary<[...Ts, U]>((rand: Random) => { const intermediate: Shrinkable<[...Ts, Shrinkable]> = this.generate(rand).map(value => { if (!Array.isArray(value)) throw new Error('method unsupported for the type') const tuple = (value as unknown) as Ts return [...tuple, genFactory(tuple).generate(rand)] }) return intermediate .andThen(interShr => { const head = interShr.value.slice(0, interShr.value.length - 1) as Ts const tail = interShr.value[interShr.value.length - 1] as Shrinkable return tail .flatMap<[...Ts, Shrinkable]>(second => new Shrinkable([...head, new Shrinkable(second)])) .shrinks() }) .map(pair => [ ...(pair.slice(0, pair.length - 1) as Ts), (pair[pair.length - 1] as Shrinkable).value, ]) }) } aggregate(genFactory: (arg: T) => Generator, minSize: number, maxSize: number): Generator { // Preserve size constraints for the resulting generator. return interval(minSize, maxSize).flatMap( size => new ArbiContainer( (rand: Random) => { // Standard aggregate logic. let shr = this.generate(rand) for (let i = 1; i < size; i++) shr = genFactory(shr.value).generate(rand) return shr }, minSize, // Use the aggregate's size for the new container constraints. maxSize ) ) } accumulate(genFactory: (arg: T) => Generator, minSize: number, maxSize: number): Generator> { // Preserve size constraints for the resulting generator. return new ArbiContainer>( (rand: Random) => { // Standard accumulate logic with shrinking. const size = rand.interval(minSize, maxSize) if (size === 0) return new Shrinkable([]) let shr = this.generate(rand) const shrArr = [shr] for (let i = 1; i < size; i++) { shr = genFactory(shr.value).generate(rand) shrArr.push(shr) } return shrinkArrayLength(shrArr, minSize) .andThen(parent => { const shrArr = parent.value if (shrArr.length === 0) return new Stream() const lastElemShr = shrArr[shrArr.length - 1] const elemShrinks = lastElemShr.shrinks() if (elemShrinks.isEmpty()) return new Stream() return elemShrinks.transform(elem => { const copy = shrArr.concat() copy[copy.length - 1] = elem return new Shrinkable(copy) }) }) .map(arr => arr.map(shr => shr.value)) }, minSize, // Use the accumulate's size for the new container constraints. maxSize ) } filter(filterer: (value: T) => boolean): Generator { // Preserve size constraints when filtering. return new ArbiContainer( (rand: Random) => { // Standard filter logic. // Potential infinite loop risk remains. while (true) { const shr = this.generate(rand) // Mistake in original code? Should filter the *returned* shrinkable, not regenerate. // if (filterer(shr.value)) return this.generate(rand).filter(filterer) // Corrected version: if (filterer(shr.value)) return shr.filter(filterer) } }, this.minSize, this.maxSize ) } /** * Updates the minimum and maximum size constraints for this container generator instance. * @param min The new minimum size. * @param max The new maximum size. */ setSize(min: number, max: number) { this.minSize = min this.maxSize = max } } /** * Type alias for the core function within a Generator that produces a Shrinkable value. * @template ARG The type of value to generate. * @param rand The random number generator instance. * @returns A Shrinkable value. */ export type GenFunction = (rand: Random) => Shrinkable