import * as Effect from "../../Effect.ts" import * as InternalArray from "../array.ts" import { done } from "../core.ts" import * as Model from "./model.ts" /** @internal */ export interface Shape { readonly fixedCount: number readonly optionalCount: number readonly repeatCount: number readonly tailCount: number readonly minimum: number } function rebuild( children: ReadonlyArray>, shape: Shape, elementStart = 0 ): Model.Sample> { let index = 0 let removable: number | undefined let itemShrinks: Model.ShrinkPull | Model.Discarded> | undefined let removableOptional = shape.repeatCount === 0 && shape.tailCount === 0 ? Math.min(shape.optionalCount, children.length - shape.minimum) : 0 const repeatEnd = shape.fixedCount + shape.repeatCount // Adapt fast-check v4.9.0's ArrayArbitrary (MIT): remove progressively smaller prefixes, // shrink the head, then visit the tail. The index loop avoids recursive slice traversal. // Unlike a suffix-local minimum, the whole-array minimum lets us remove interior elements // even when a retained prefix already accounts for part of the required length. // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/ArrayArbitrary.ts const loop = (): Model.ShrinkPull>> => Effect.suspend(() => { if (removableOptional > 0) { const count = removableOptional removableOptional = Math.floor(removableOptional / 2) return Effect.succeed(rebuild(children.slice(0, -count), { ...shape, fixedCount: shape.fixedCount - count, optionalCount: shape.optionalCount - count })) } while (index < children.length) { removable ??= index >= shape.fixedCount && index < repeatEnd ? Math.min(repeatEnd - index, children.length - shape.minimum) : 0 if (removable > 0) { const count = removable removable = Math.floor(removable / 2) return Effect.succeed(rebuild(children.slice(0, index).concat(children.slice(index + count)), { ...shape, repeatCount: shape.repeatCount - count })) } if (index >= elementStart) itemShrinks ??= children[index].shrinks?.() if (itemShrinks !== undefined) { return Effect.matchCauseEffect(itemShrinks, { onFailure: Model.onTypedFailure(() => { itemShrinks = undefined removable = undefined index++ return loop() }), onSuccess: (item): Effect.Effect>> => { if (item._tag === "Discarded") return Effect.succeed(item) return Effect.succeed(rebuild(InternalArray.replaceAt(children, index, item), shape, index)) } }) } index++ removable = undefined } return done() }) return Model.makeSample(children.map((child) => child.value), loop()) } /** @internal */ export function sample( children: ReadonlyArray>, shape: Shape, shrinks = true ): Model.Sample> { const value = children.map((child) => child.value) if (!shrinks) return Model.makeSample(value) if (children.length <= shape.minimum || shape.repeatCount === 0 && shape.optionalCount === 0) { return Model.productSample(children, (children) => children.map((child) => child.value)) } return Model.makeSampleWithLazyShrinks(value, () => rebuild(children.map(Model.retain), shape).shrinks) }