1import type {
2 Arbitrary,
3 ArrayOptions,
4 CheckOptions,
5 CheckResult,
6 GlobalOptions,
7 PropertyError,
8 PropertyFailure,
9 Replay,
10 ReturnedFalse,
11 SampleError,
12 SampleOptions,
13 SchemaOptions
14} from "../../Arbitrary.ts"
15import * as Effect from "../../Effect.ts"
16import * as Option from "../../Option.ts"
17import { pipeArguments } from "../../Pipeable.ts"
18import * as Random from "../../Random.ts"
19import type * as Result from "../../Result.ts"
20import * as Scheduler from "../../Scheduler.ts"
21import type * as Schema from "../../Schema.ts"
22import { done } from "../core.ts"
23import * as InternalRecord from "../record.ts"
24import * as Arrays from "./array.ts"
25import * as Model from "./model.ts"
26import * as Compiler from "./schema.ts"
27
28/** @internal */
29export const TypeId = "~effect/arbitrary/Arbitrary"
30
31let globalOptions: GlobalOptions = {}
32
33export const configureGlobal = (options: GlobalOptions) => {
34 globalOptions = { check: { ...options.check }, sample: { ...options.sample } }
35}
36
37type FailureTag = PropertyFailure<unknown>["_tag"]
38
39interface ReplayData {
40 readonly seed: string | number
41 readonly attempt: number
42 readonly size: number
43 readonly path: ReadonlyArray<number>
44 readonly failureTag: FailureTag
45}
46
47const ArbitraryProto = {
48 [TypeId]: TypeId,
49 pipe() {
50 return pipeArguments(this, arguments)
51 }
52}
53
54function make<A>(generator: Model.Generator<A>): Arbitrary<A> {
55 return Object.create(ArbitraryProto, {
56 gen: { value: generator }
57 })
58}
59
60function makeReplay(data: ReplayData): Replay {
61 return JSON.stringify([
62 typeof data.seed === "number" ? 0 : 1,
63 globalThis.String(data.seed),
64 data.attempt,
65 data.size,
66 data.path,
67 data.failureTag
68 ])
69}
70
71function replayData(replay: Replay): ReplayData {
72 const encoded = JSON.parse(replay) as [0 | 1, string, number, number, ReadonlyArray<number>, FailureTag]
73 return {
74 seed: encoded[0] === 0 ? globalThis.Number(encoded[1]) : encoded[1],
75 attempt: encoded[2],
76 size: encoded[3],
77 path: encoded[4],
78 failureTag: encoded[5]
79 }
80}
81
82function natural(value: number | undefined, fallback: number, label: string): number {
83 const out = value ?? fallback
84 if (!Number.isSafeInteger(out) || out < 0) throw new Error(`${label} must be a non-negative safe integer`)
85 return out
86}
87
88function positive(value: number | undefined, fallback: number, label: string): number {
89 const out = natural(value, fallback, label)
90 if (out === 0) throw new Error(`${label} must be greater than zero`)
91 return out
92}
93
94interface SeedState {
95 readonly first: number
96 readonly second: number
97}
98
99function mix32(value: number): number {
100 // MurmurHash3 fmix32 by Austin Appleby, dedicated to the public domain.
101 value ^= value >>> 16
102 value = Math.imul(value, 0x85ebca6b)
103 value ^= value >>> 13
104 value = Math.imul(value, 0xc2b2ae35)
105 return (value ^ value >>> 16) >>> 0
106}
107
108function hashSeed(seed: string | number): SeedState {
109 const value = `${typeof seed}:${seed}`
110 let first = 0x811c9dc5
111 let second = 0x9e3779b9
112 for (let index = 0; index < value.length; index++) {
113 const code = value.charCodeAt(index)
114 first = Math.imul(first ^ code, 0x01000193)
115 second = Math.imul(second ^ code ^ index, 0x85ebca6b)
116 }
117 return {
118 first: mix32(first),
119 second: mix32(second ^ value.length)
120 }
121}
122
123function rotateLeft(value: number, shift: number): number {
124 return (value << shift | value >>> (32 - shift)) >>> 0
125}
126
127function makeGenerationRandom(
128 initialState0: number,
129 initialState1: number,
130 initialState2: number,
131 initialState3: number
132): Model.GenerationRandom {
133 let state0 = initialState0
134 let state1 = initialState1
135 let state2 = initialState2
136 let state3 = initialState3
137 // xoshiro128** 1.1 by David Blackman and Sebastiano Vigna, dedicated to the public domain.
138 // https://prng.di.unimi.it/xoshiro128starstar.c
139 const nextUint32 = () => {
140 const result = Math.imul(rotateLeft(Math.imul(state1, 5), 7), 9) >>> 0
141 const temporary = state1 << 9
142 state2 ^= state0
143 state3 ^= state1
144 state1 ^= state2
145 state0 ^= state3
146 state2 ^= temporary
147 state3 = rotateLeft(state3, 11)
148 return result
149 }
150 const nextDoubleUnsafe = () => {
151 const high = nextUint32() >>> 5
152 const low = nextUint32() >>> 6
153 return (high * 0x4000000 + low) / 0x20000000000000
154 }
155 return {
156 nextUint32,
157 nextDoubleUnsafe,
158 clone: () => makeGenerationRandom(state0, state1, state2, state3),
159 copyFrom: (source) => {
160 const snapshot = source.snapshot()
161 state0 = snapshot[0]
162 state1 = snapshot[1]
163 state2 = snapshot[2]
164 state3 = snapshot[3]
165 },
166 snapshot: () => [state0, state1, state2, state3]
167 }
168}
169
170function makeAttemptRandom(seed: SeedState, attempt: number): Model.GenerationRandom {
171 // This provides the same per-run isolation targeted by fast-check v4.9.0's jump-before-toss strategy (MIT), while
172 // deriving the attempt state directly so replay can jump to it without executing preceding attempts.
173 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/check/runner/Tosser.ts
174 const attemptLow = attempt >>> 0
175 const attemptHigh = Math.floor(attempt / 0x100000000) >>> 0
176 let state0 = mix32(seed.first ^ attemptLow ^ Math.imul(attemptHigh, 0x9e3779b9))
177 const state1 = mix32(seed.second ^ attemptHigh ^ Math.imul(attemptLow, 0x85ebca6b))
178 const state2 = mix32(seed.first ^ attemptHigh ^ Math.imul(attemptLow, 0xc2b2ae35) ^ 0x243f6a88)
179 const state3 = mix32(seed.second ^ attemptLow ^ Math.imul(attemptHigh, 0x27d4eb2f) ^ 0xb7e15162)
180 if ((state0 | state1 | state2 | state3) === 0) state0 = 0x9e3779b9
181 return makeGenerationRandom(state0, state1, state2, state3)
182}
183
184const generateAttemptRaw = <A>(
185 generator: Model.Generator<A>,
186 seed: SeedState,
187 attempt: number,
188 size: number,
189 shrinks: boolean
190): Model.Generation<A> => {
191 const biasFactor = 2 + Math.floor(Math.log10(attempt + 1))
192 return generator.generate({
193 size,
194 shrinks,
195 // This is fast-check v4.9.0's run-dependent numeric bias schedule (MIT). It makes short checks edge-heavy while
196 // progressively dedicating more attempts to the complete domain, without becoming part of the public API.
197 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/check/property/IRawProperty.ts#L92-L95
198 biasFactor,
199 nullPrototype: (attempt + 1) % (biasFactor * 4) === 0,
200 random: makeAttemptRandom(seed, attempt),
201 budget: { remaining: generator.minCost + size }
202 })
203}
204
205const generateAttempt = <A>(
206 generator: Model.Generator<A>,
207 seed: SeedState,
208 attempt: number,
209 size: number,
210 shrinks: boolean
211): Effect.Effect<Model.Attempt<A>> =>
212 Model.toEffectGeneration(generateAttemptRaw(generator, seed, attempt, size, shrinks))
213
214const resolveMasterSeed = (seed: string | number | undefined): Effect.Effect<string | number> =>
215 seed === undefined ? Random.nextInt : Effect.succeed(seed)
216
217/** @internal */
218export function schema<S extends Schema.Constraint>(
219 schema: S,
220 options?: SchemaOptions<S["Type"]>
221): Arbitrary<S["Type"]> {
222 const compiled = Compiler.compile(schema)
223 const shrink = options?.shrink
224 if (shrink === undefined) return make(compiled)
225 const validate = Compiler.compileValidator(schema)
226 return make(Model.makeGenerator(
227 compiled.minCost,
228 (state) =>
229 Model.mapGeneration(compiled.generate(state), (attempt) =>
230 attempt._tag === "Discarded" || !state.shrinks
231 ? attempt
232 : Model.sampleFromValidatedShrink(attempt.value, shrink, validate))
233 ))
234}
235
236/** @internal */
237export function constant<A>(value: A): Arbitrary<A> {
238 return make(Model.makeGenerator(0, () => Model.makeSample(value)))
239}
240
241/** @internal */
242export function map<A, B>(self: Arbitrary<A>, f: (value: A) => B): Arbitrary<B> {
243 return make(Model.makeGenerator(
244 self.gen.minCost,
245 (state) =>
246 Model.mapGeneration(
247 self.gen.generate(state),
248 (attempt) => attempt._tag === "Discarded" ? attempt : Model.mapSample(attempt, f)
249 )
250 ))
251}
252
253/** @internal */
254export function filter<A>(self: Arbitrary<A>, predicate: (value: A) => boolean): Arbitrary<A> {
255 return make(Model.makeGenerator(
256 self.gen.minCost,
257 (state) =>
258 Model.mapGeneration(self.gen.generate(state), (attempt) => {
259 if (attempt._tag === "Discarded") return attempt
260 if (!state.shrinks) return predicate(attempt.value) ? attempt : Model.discarded
261 const sample = Model.filterSample(attempt, predicate)
262 return sample ?? Model.discarded
263 })
264 ))
265}
266
267/** @internal */
268export function filterMap<A, B, X>(
269 self: Arbitrary<A>,
270 f: (value: A) => Result.Result<B, X>
271): Arbitrary<B> {
272 const apply = (value: A): Option.Option<B> => {
273 const result = f(value)
274 return result._tag === "Success" ? Option.some(result.success) : Option.none()
275 }
276 return make(Model.makeGenerator(
277 self.gen.minCost,
278 (state) =>
279 state.shrinks
280 ? Model.filterMapGeneration(self.gen.generate(state), apply)
281 : Model.mapGeneration(self.gen.generate(state), (attempt) => {
282 if (attempt._tag === "Discarded") return attempt
283 const result = apply(attempt.value)
284 return Option.isSome(result) ? Model.makeSample(result.value) : Model.discarded
285 })
286 ))
287}
288
289/** @internal */
290export function array<A>(item: Arbitrary<A>, options?: ArrayOptions): Arbitrary<Array<A>> {
291 const minimum = options?.minLength ?? 0
292 const maximum = options?.maxLength ?? 0xffffffff
293 if (
294 !Number.isInteger(minimum) || minimum < 0 || minimum > 0xffffffff ||
295 !Number.isInteger(maximum) || maximum < minimum || maximum > 0xffffffff
296 ) {
297 throw new RangeError("Arbitrary.array: expected 0 <= minLength <= maxLength <= 4294967295 with integer lengths")
298 }
299 const child = item.gen
300 const minCost = minimum === 0 ? 0 : minimum * child.minCost
301 return make(Model.makeGenerator(minCost, (state) => {
302 const upper = Math.min(
303 maximum,
304 Math.max(minimum, state.size),
305 child.minCost > 0 ? Math.floor(state.budget.remaining / child.minCost) : maximum
306 )
307 if (upper < minimum) return Model.discarded
308 const length = Model.randomLength(state, minimum, upper)
309 const children = Array.from({ length }, () => child)
310 const order = length < 2 ? undefined : Model.shuffle(state, children.keys())
311 const itemState = state.size >= length ? state : { ...state, size: length }
312 return Model.mapComputation(
313 Model.generateProduct(children, itemState, order, length === 0 ? 0 : length * child.minCost),
314 (samples) =>
315 samples === undefined ? Model.discarded : Arrays.sample<A>(samples, {
316 fixedCount: 0,
317 optionalCount: 0,
318 repeatCount: length,
319 tailCount: 0,
320 minimum
321 }, state.shrinks)
322 )
323 }))
324}
325
326/** @internal */
327export function all(
328 input: Iterable<Arbitrary<any>> | Record<string, Arbitrary<any>>
329): Arbitrary<any> {
330 const iterable = Symbol.iterator in input
331 const keys = iterable ? undefined : Object.keys(input)
332 const members = iterable ? Array.from(input as Iterable<Arbitrary<any>>) : keys!.map((key) => input[key])
333 const generators = members.map((member) => member.gen)
334 let minCost = 0
335 for (let index = 0; index < generators.length; index++) minCost += generators[index].minCost
336 const indexes = generators.map((_, index) => index)
337 return make(Model.makeGenerator(
338 minCost,
339 (state) => {
340 const order = indexes.length < 2 ? undefined : Model.shuffle(state, indexes)
341 return Model.mapComputation(
342 Model.generateProduct(generators, state, order, minCost),
343 (samples) => {
344 if (samples === undefined) return Model.discarded
345 return Model.productSample(samples, (samples) => {
346 if (keys === undefined) return samples.map((sample) => sample.value)
347 const out = Model.makeObject(state.nullPrototype)
348 for (let index = 0; index < keys.length; index++) {
349 InternalRecord.assignProperty(out, keys[index], samples[index].value)
350 }
351 return out
352 })
353 }
354 )
355 }
356 ))
357}
358
359function makeGenerationState(
360 state: Model.GenerationState,
361 random: Model.GenerationRandom,
362 remaining: number
363): Model.GenerationState {
364 return {
365 size: state.size,
366 shrinks: state.shrinks,
367 biasFactor: state.biasFactor,
368 nullPrototype: state.nullPrototype,
369 random,
370 budget: { remaining }
371 }
372}
373
374function flatMapSourcePull<A, B>(
375 source: Model.ShrinkPull<Model.Attempt<A>>,
376 f: (value: A) => Arbitrary<B>,
377 checkpoint: Model.GenerationRandom,
378 state: Model.GenerationState,
379 residual: number
380): Model.ShrinkPull<Model.Attempt<B>> {
381 const stack: Array<Model.ShrinkPull<Model.Attempt<A>>> = [source]
382 const loop = (): Model.ShrinkPull<Model.Attempt<B>> =>
383 Effect.suspend(() => {
384 const current = stack[stack.length - 1]
385 if (current === undefined) return done()
386 return Effect.matchEffect(current, {
387 onFailure: () => {
388 stack.pop()
389 return loop()
390 },
391 onSuccess: (sourceAttempt) => {
392 if (sourceAttempt._tag === "Discarded") return Effect.succeed<Model.Attempt<B>>(sourceAttempt)
393 const sourceSample = sourceAttempt
394 const target = f(sourceSample.value).gen
395 const targetState = makeGenerationState(state, checkpoint.clone(), residual + target.minCost)
396 return Effect.flatMapEager(Model.toEffectGeneration(target.generate(targetState)), (attempt) => {
397 if (attempt._tag === "Discarded") {
398 if (sourceSample.shrinks !== undefined) stack.push(sourceSample.shrinks)
399 return Effect.succeed<Model.Attempt<B>>(Model.discarded)
400 }
401 return Effect.succeed<Model.Attempt<B>>(
402 flatMapSample(sourceSample, attempt, f, checkpoint, state, residual)
403 )
404 })
405 }
406 })
407 })
408 return loop()
409}
410
411function flatMapSample<A, B>(
412 source: Model.Sample<A>,
413 target: Model.Sample<B>,
414 f: (value: A) => Arbitrary<B>,
415 checkpoint: Model.GenerationRandom,
416 state: Model.GenerationState,
417 residual: number
418): Model.Sample<B> {
419 // Source-first shrinking followed by permanently closing the source after a dependent shrink follows fast-check
420 // v4.9.0's ChainArbitrary topology (MIT).
421 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/check/arbitrary/definition/Arbitrary.ts#L147-L228
422 if (source.shrinks === undefined) return target
423 const sourceShrinks = flatMapSourcePull(source.shrinks, f, checkpoint, state, residual)
424 const shrinks = target.shrinks === undefined
425 ? sourceShrinks
426 : Model.concatPulls([sourceShrinks, target.shrinks])
427 return Model.makeSample(target.value, shrinks)
428}
429
430/** @internal */
431export function flatMap<A, B>(self: Arbitrary<A>, f: (value: A) => Arbitrary<B>): Arbitrary<B> {
432 return make(Model.makeGenerator(self.gen.minCost, (state) => {
433 if (!state.shrinks) {
434 return Model.flatMapGeneration(self.gen.generate(state), (source) => {
435 if (source._tag === "Discarded") return source
436 const target = f(source.value).gen
437 const residual = state.budget.remaining
438 state.budget.remaining = residual + target.minCost
439 return Model.mapGeneration(target.generate(state), (attempt) => {
440 state.budget.remaining = Math.min(residual, state.budget.remaining)
441 return attempt
442 })
443 })
444 }
445
446 // Unlike fast-check's pre-source checkpoint, Effect captures the isolated PRNG after source generation so a
447 // smaller source changes the dependent constraint without rerolling its choices.
448 const sourceState = makeGenerationState(state, state.random.clone(), state.budget.remaining)
449 return Model.flatMapGeneration(self.gen.generate(sourceState), (source) => {
450 if (source._tag === "Discarded") {
451 state.random.copyFrom(sourceState.random)
452 state.budget.remaining = sourceState.budget.remaining
453 return source
454 }
455 const target = f(source.value).gen
456 const residual = sourceState.budget.remaining
457 const checkpoint = sourceState.random.clone()
458 const targetState = makeGenerationState(state, checkpoint.clone(), residual + target.minCost)
459 return Model.mapGeneration(target.generate(targetState), (attempt) => {
460 state.random.copyFrom(targetState.random)
461 state.budget.remaining = Math.min(residual, targetState.budget.remaining)
462 return attempt._tag === "Discarded" ? attempt : flatMapSample(source, attempt, f, checkpoint, state, residual)
463 })
464 })
465 }))
466}
467
468/** @internal */
469export const sampleEffect = Effect.fnUntraced(function*<A>(self: Arbitrary<A>, options?: SampleOptions) {
470 const defaults = globalOptions.sample
471 const count = natural(options?.count ?? defaults?.count, 10, "count")
472 const size = natural(options?.size ?? defaults?.size, 10, "size")
473 const maxDiscards = natural(options?.maxDiscards ?? defaults?.maxDiscards, Math.max(100, count * 10), "maxDiscards")
474 const seedOption = options?.seed ?? defaults?.seed
475 const maxOpsBeforeYield = yield* Scheduler.MaxOpsBeforeYield
476 const seed = yield* resolveMasterSeed(seedOption)
477 const seedState = hashSeed(seed)
478 const values: Array<A> = []
479 let discards = 0
480 let attemptIndex = 0
481 let attemptsSinceYield = 0
482 while (values.length < count) {
483 const generated = generateAttemptRaw(self.gen, seedState, attemptIndex++, size, false)
484 const attempt = Model.isAttempt(generated) ? generated : yield* generated
485 if (attempt._tag === "Generated") {
486 values.push(attempt.value)
487 } else if (++discards > maxDiscards) {
488 const error: SampleError = { _tag: "SampleError", generated: values.length, discards, seed }
489 return yield* Effect.fail(error)
490 }
491 if (++attemptsSinceYield >= maxOpsBeforeYield) {
492 attemptsSinceYield = 0
493 yield* Effect.yieldNow
494 }
495 }
496 return values
497})
498
499const passedProperty = { _tag: "Passed" } as const
500const returnedFalse: ReturnedFalse = { _tag: "ReturnedFalse" }
501
502const evaluateProperty = <A, E, R>(
503 property: (value: A) => boolean | Effect.Effect<boolean, E, R>,
504 value: A
505): Effect.Effect<typeof passedProperty | PropertyFailure<E>, never, R> => {
506 const output = property(value)
507 if (!Effect.isEffect(output)) return Effect.succeed(output === true ? passedProperty : returnedFalse)
508 return Effect.matchEager(output, {
509 onFailure: (error): PropertyError<E> => ({ _tag: "PropertyError", error }),
510 onSuccess: (success) => success === true ? passedProperty : returnedFalse
511 })
512}
513
514const pullNext = <A>(pull: Model.ShrinkPull<Model.Attempt<A>>): Effect.Effect<Model.Attempt<A> | undefined> =>
515 Effect.catch(pull, () => Effect.succeed(undefined))
516
517const shrink = Effect.fnUntraced(function*<A, E, R>(
518 initial: Model.Sample<A>,
519 initialFailure: PropertyFailure<E>,
520 property: (value: A) => boolean | Effect.Effect<boolean, E, R>,
521 maximum: number
522) {
523 // The first-failing-child traversal and its sibling-index path follow fast-check v4.9.0's counterexample-path model
524 // (MIT).
525 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/check/runner/RunnerIterator.ts
526 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/src/check/runner/utils/PathWalker.ts
527 let current = initial
528 let failure = initialFailure
529 let inspected = 0
530 const path: Array<number> = []
531 while (inspected < maximum) {
532 let index = 0
533 let found: Model.Sample<A> | undefined
534 while (inspected < maximum) {
535 if (current.shrinks === undefined) break
536 const candidate = yield* pullNext(current.shrinks)
537 if (candidate === undefined) break
538 inspected++
539 const attempt = candidate
540 if (attempt._tag === "Discarded") continue
541 const outcome = yield* evaluateProperty(property, attempt.value)
542 if (outcome._tag === initialFailure._tag) {
543 found = attempt
544 failure = outcome
545 path.push(index)
546 break
547 }
548 index++
549 }
550 if (found === undefined) break
551 current = found
552 }
553 return { current, failure, shrinks: path.length, path }
554})
555
556const followReplay = Effect.fnUntraced(function*<A, E, R>(
557 initial: Model.Sample<A>,
558 initialFailure: PropertyFailure<E>,
559 path: ReadonlyArray<number>,
560 property: (value: A) => boolean | Effect.Effect<boolean, E, R>
561) {
562 let current = initial
563 let failure = initialFailure
564 for (const targetIndex of path) {
565 if (current.shrinks === undefined) return { _tag: "ReplayMismatch", reason: "ShrinkPathUnavailable" } as const
566 let selected: Model.Sample<A> | undefined
567 let index = 0
568 while (index <= targetIndex) {
569 const candidate = yield* pullNext(current.shrinks)
570 if (candidate === undefined) return { _tag: "ReplayMismatch", reason: "ShrinkPathUnavailable" } as const
571 if (candidate._tag === "Discarded") continue
572 if (index === targetIndex) selected = candidate
573 index++
574 }
575 const outcome = yield* evaluateProperty(property, selected!.value)
576 if (outcome._tag !== initialFailure._tag) {
577 return { _tag: "ReplayMismatch", reason: "ShrinkPassed" } as const
578 }
579 current = selected!
580 failure = outcome
581 }
582 return { _tag: "Replayed", current, failure } as const
583})
584
585/** @internal */
586export const checkEffect = Effect.fnUntraced(function*<A, E, R>(
587 self: Arbitrary<A>,
588 property: (value: A) => boolean | Effect.Effect<boolean, E, R>,
589 options?: CheckOptions
590): Effect.fn.Return<CheckResult<A, E>, never, R> {
591 const replay = options?.replay
592 if (replay !== undefined) {
593 const data = replayData(replay)
594 const attempt = yield* generateAttempt(self.gen, hashSeed(data.seed), data.attempt, data.size, true)
595 if (attempt._tag === "Discarded") return { _tag: "ReplayMismatch", reason: "AttemptDiscarded" }
596 const outcome = yield* evaluateProperty(property, attempt.value)
597 if (outcome._tag === "Passed") return { _tag: "ReplayMismatch", reason: "PropertyPassed" }
598 if (outcome._tag !== data.failureTag) return { _tag: "ReplayMismatch", reason: "ShrinkPassed" }
599 const replayed = yield* followReplay(attempt, outcome, data.path, property)
600 if (replayed._tag === "ReplayMismatch") return replayed
601 return {
602 _tag: "Falsified",
603 initialInput: attempt.value,
604 shrunkInput: replayed.current.value,
605 failure: replayed.failure,
606 runs: 1,
607 discards: 0,
608 shrinks: data.path.length,
609 replay
610 }
611 }
612
613 const defaults = globalOptions.check
614 const runsTarget = positive(options?.runs ?? defaults?.runs, 100, "runs")
615 const size = natural(options?.size ?? defaults?.size, 10, "size")
616 const maxDiscards = natural(
617 options?.maxDiscards ?? defaults?.maxDiscards,
618 Math.max(100, runsTarget * 10),
619 "maxDiscards"
620 )
621 const maxShrinks = natural(options?.maxShrinks ?? defaults?.maxShrinks, 100, "maxShrinks")
622 const seedOption = options?.seed ?? defaults?.seed
623 const maxOpsBeforeYield = yield* Scheduler.MaxOpsBeforeYield
624 const seed = yield* resolveMasterSeed(seedOption)
625 const seedState = hashSeed(seed)
626 let runs = 0
627 let discards = 0
628 let attemptIndex = 0
629 while (runs < runsTarget) {
630 const currentAttempt = attemptIndex++
631 const currentSize = runsTarget === 1 ? size : Math.round(runs * size / (runsTarget - 1))
632 const attempt = yield* generateAttempt(self.gen, seedState, currentAttempt, currentSize, true)
633 if (attempt._tag === "Discarded") {
634 if (++discards > maxDiscards) return { _tag: "Exhausted", runs, discards, seed }
635 if (maxOpsBeforeYield <= 1 || discards % maxOpsBeforeYield === 0) yield* Effect.yieldNow
636 continue
637 }
638 const outcome = yield* evaluateProperty(property, attempt.value)
639 if (outcome._tag === "Passed") {
640 runs++
641 continue
642 }
643 const minimized = yield* shrink(attempt, outcome, property, maxShrinks)
644 return {
645 _tag: "Falsified",
646 initialInput: attempt.value,
647 shrunkInput: minimized.current.value,
648 failure: minimized.failure,
649 runs: runs + 1,
650 discards,
651 shrinks: minimized.shrinks,
652 replay: makeReplay({
653 seed,
654 attempt: currentAttempt,
655 size: currentSize,
656 path: minimized.path,
657 failureTag: outcome._tag
658 })
659 }
660 }
661 return { _tag: "Passed", runs, discards }
662})