123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208moduleRandom=PRNG.Splitmix.PuremoduleForest=Tree.Forest(* Intuitively, a generator produces a single value from a
pseudo-random generator, commonly represented by the type
[Random.Pure.t -> 'a]. In this library, we extend this concept in
two significant ways.
1) Instead of generating a singular value, we generate a tree of
values, modifying the type to [Random.Pure.t -> 'a Tree.t]. The
tree's root represents the initially generated value, with its
children representing smaller, "shrunk" candidates. These
candidates are utilized by the shrinking algorithm to find smaller
values that still meet certain criteria if the initially generated
value does not (see [Tree.shrink]).
The choice of [Random.Pure] for number generation is deliberate. It
ensures that the [bind] function remains pure, maintaining the
independence of values produced by a function [f] in [bind x f]
from those generated by [x]. Consequently, re-executing [f] with
smaller values of [x] will yield the same values as the original
[x]. For instance, if [x] generates a positive number [n], and [f]
subsequently generates [n] numbers, then calling [f] with [n' < n]
will result in [f] producing the same first [n'] numbers.
2) The evolution to [Random.Pure,t -> 'a Forest.t] facilitates more
refined shrinkers, especially for recursive data types like
lists. When shrinking recursive generators, it might be beneficial
to bypass certain values. For example, when shrinking a list of
size 3 (e.g., [1 2 3]), a shrinker might opt to test a size 2
list. But which sublist should it choose? The default might be [1
2], but employing [Forest.t] instead of [Tree.t] enables the
shrinker to consider [1 3] or [2 3] as well. This approach implies
that during shrinking, the generator might yield multiple values
instead of just one. Continuing with the list example, after
deciding to test a list of size 2, the first list value could be
either 1 or 2, leading to the conceptualization of [Forest.t] as a
sequence of [Tree.t]. Note, however, that a [Forest.t] is never
empty.
It's essential to maintain that the sequence's size should always
be one upon value generation (refer to [run]). However, if the
generator is invoked recursively during shrinking, the sequence
size might increase. This behavior is not enforced by the type
system, placing the onus on the generator's developer to uphold
this invariant. *)type'at=Random.t->'aForest.tletreturn:'a->'at=funvalue_rs->Forest.returnvalueletmake:'a->('a->'aSeq.t)->'at=funrootmake_children_rs->Forest.makerootmake_childrenletbind:'at->('a->'bt)->'bt=fungenfrs->(* Split guarantees that [rs_left] and [rs_right] are independent
allowing [f] to be called multiple times with different values
for [a] and still produces the same values. *)letrs_left,rs_right=Random.splitrsinletforest=genrs_leftinForest.bindforest(funa->fars_right)letmap:('a->'b)->'at->'bt=funfgenrs->Forest.mapf(genrs)(* This function does a lookup on the generator given and always
returns the generated value. It can be used to implement shrinkers.
This lookup is correct only if the generator is given as the left
parameter of a bind. *)letroot(gen:'at)frs=(* The split call mimic what bind is doing so that the value given
to the function [f] is indeed the one that would be produced with
a bind. *)letrs_left,_=Random.splitrsinForest.first(genrs_left)|>Tree.root|>Fun.flipfrsmoduleSyntax=structlet(let*)xf=bindxflet(let*!)=rootletreturn=returnend(* When implementing shrinkers, the generator may produce a sequence
of values. This function make a single generator out a sequence of
generators. *)letsequence:'at->'atSeq.t->'at=fungenseqrs->letgen=genrsinletseq=Seq.map(fungen->genrs)seqinForest.sequencegenseq(* This module can be used to define better shrinkers by defining a
merging strategy. *)moduleMerge=struct(* For abstraction purpose, we hide the merge function over
trees. In the future, this module could be extended with other
strategies. *)type'at='aTree.tSeq.t->'aTree.tSeq.t->'aTree.tSeq.tletdefault=Seq.appendletdrop_left_y=yletdrop_rightx_=xletof_compare:compare:('a->'a->int)->'at=fun~compare->letcompareleftright=compare(Tree.rootleft)(Tree.rootright)inSeq.sorted_mergecompareend(* Set the merging behavior for all the trees defined.
Since [sequence] does not change the merge strategy, if this
function is called before calling [sequence], each tree of the
forest may have there own merge strategy. *)letwith_merge:'aMerge.t->'at->'at=funmergegenrs->Forest.map_tree(funtree->Tree.with_merge~mergetree)(genrs)letz_range:?origin:Z.t->min:Z.t->max:Z.t->unit->Z.tt=fun?origin~min~max()rs->letopenZ.Compareinifmax<=minthenForest.returnminelseletstart=letupper_bound=Z.succ(Z.submaxmin)inletrs=Obj.magicrsinZ.random_int_gen~fill:(funbytesposlen->PRNG.Splitmix.State.bytesrsbytesposlen)upper_boundinletinitial=Z.addminstartinletorigin=Option.valueorigin~default:(ifmin<=Z.zero&&Z.zero<=maxthenZ.zeroelsemin)inTree.binary_search~initial~origin()|>Forest.liftletfloat_range:?exhaustive_search_digits:int->?precision_digits:int->?origin:float->min:float->max:float->unit->floatt=fun?exhaustive_search_digits?precision_digits?origin~min~max()rs->letorigin=Option.valueorigin~default:(ifmin<=0.&&0.<=maxthen0.elsemin)inifmin>=maxthenreturnminrselseifmax-.min<=1.thenletinitial,_=Random.float(max-.min)rsinTree.fractional_search?exhaustive_search_digits?precision_digits~initial~origin()|>Forest.lift|>Forest.map(funx->x+.min)elseletrs,rs'=Random.splitrsinlet_,mini=Float.modfmininlet_,maxi=Float.modfmaxinletoriginf,origini=Float.modforigininletshift=Z.of_floatminiinletforest=z_range~origin:(Z.sub(Z.of_floatorigini)shift)~min:Z.zero~max:(Z.sub(Z.of_floatmaxi)shift)()rsinletfractional=Random.float1.rs'|>fstinletff,fi=Float.modffractionalinletfractional_forest=Tree.fractional_search?exhaustive_search_digits?precision_digits~initial:ff~origin:originf()|>Forest.liftinForest.bindforest(funx->letvalue=Z.addx(Z.of_floatfi)|>Z.to_floatinForest.map(funfractional->Float.maxmin(value+.fractional+.min)|>Float.minmax)fractional_forest)letcrunchi(gen:'at):'at=funrs->letforest=genrsinForest.crunchiforestletshrink=Tree.shrink(* [t] is a runnable monad. Hence to run a generator, one needs to
provide an initial state for the random generator. We check the
validity of the generator by checking whether the [Forest] is a
singleton. Having multiple trees in the forest is allowed only
during shrinking, not while generated values. *)letrun?(on_failure=failwith)genstate=letmessage="[Gen.run] was called with an erroneous generator. The generator is \
expected to return a single value. Instead: multiple values were \
returned. You should probably fix your generator or provide a \
[on_failure] argument to [Gen.run]."inletforest=genstateinifForest.is_singletonforestthenForest.firstforestelseon_failuremessage