1import * as Effect from "../../Effect.ts"
2import * as InternalArray from "../array.ts"
3import { done } from "../core.ts"
4import * as Model from "./model.ts"
5
6/** @internal */
7export interface Shape {
8 readonly fixedCount: number
9 readonly optionalCount: number
10 readonly repeatCount: number
11 readonly tailCount: number
12 readonly minimum: number
13}
14
15function rebuild<A>(
16 children: ReadonlyArray<Model.Retained<A>>,
17 shape: Shape,
18 elementStart = 0
19): Model.Sample<Array<A>> {
20 let index = 0
21 let removable: number | undefined
22 let itemShrinks: Model.ShrinkPull<Model.Retained<A> | Model.Discarded> | undefined
23 let removableOptional = shape.repeatCount === 0 && shape.tailCount === 0
24 ? Math.min(shape.optionalCount, children.length - shape.minimum)
25 : 0
26 const repeatEnd = shape.fixedCount + shape.repeatCount
27 // Adapt fast-check v4.9.0's ArrayArbitrary (MIT): remove progressively smaller prefixes,
28 // shrink the head, then visit the tail. The index loop avoids recursive slice traversal.
29 // Unlike a suffix-local minimum, the whole-array minimum lets us remove interior elements
30 // even when a retained prefix already accounts for part of the required length.
31 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/arbitrary/_internals/ArrayArbitrary.ts
32 const loop = (): Model.ShrinkPull<Model.Attempt<Array<A>>> =>
33 Effect.suspend(() => {
34 if (removableOptional > 0) {
35 const count = removableOptional
36 removableOptional = Math.floor(removableOptional / 2)
37 return Effect.succeed(rebuild(children.slice(0, -count), {
38 ...shape,
39 fixedCount: shape.fixedCount - count,
40 optionalCount: shape.optionalCount - count
41 }))
42 }
43 while (index < children.length) {
44 removable ??= index >= shape.fixedCount && index < repeatEnd
45 ? Math.min(repeatEnd - index, children.length - shape.minimum)
46 : 0
47 if (removable > 0) {
48 const count = removable
49 removable = Math.floor(removable / 2)
50 return Effect.succeed(rebuild(children.slice(0, index).concat(children.slice(index + count)), {
51 ...shape,
52 repeatCount: shape.repeatCount - count
53 }))
54 }
55 if (index >= elementStart) itemShrinks ??= children[index].shrinks?.()
56 if (itemShrinks !== undefined) {
57 return Effect.matchEffect(itemShrinks, {
58 onFailure: () => {
59 itemShrinks = undefined
60 removable = undefined
61 index++
62 return loop()
63 },
64 onSuccess: (item): Effect.Effect<Model.Attempt<Array<A>>> => {
65 if (item._tag === "Discarded") return Effect.succeed(item)
66 return Effect.succeed(rebuild(InternalArray.replaceAt(children, index, item), shape, index))
67 }
68 })
69 }
70 index++
71 removable = undefined
72 }
73 return done()
74 })
75 return Model.makeSample(children.map((child) => child.value), loop())
76}
77
78/** @internal */
79export function sample<A>(
80 children: ReadonlyArray<Model.Sample<A>>,
81 shape: Shape,
82 shrinks = true
83): Model.Sample<Array<A>> {
84 const value = children.map((child) => child.value)
85 if (!shrinks) return Model.makeSample(value)
86 if (children.length <= shape.minimum || shape.repeatCount === 0 && shape.optionalCount === 0) {
87 return Model.productSample(children, (children) => children.map((child) => child.value))
88 }
89 return Model.makeSampleWithLazyShrinks(value, () => rebuild(children.map(Model.retain), shape).shrinks)
90}