123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578(**************************************************************************)(* This file is part of the Codex semantics library. *)(* *)(* Copyright (C) 2013-2025 *)(* CEA (Commissariat à l'énergie atomique et aux énergies *)(* alternatives) *)(* *)(* you can redistribute it and/or modify it under the terms of the GNU *)(* Lesser General Public License as published by the Free Software *)(* Foundation, version 2.1. *)(* *)(* It is distributed in the hope that it will be useful, *)(* but WITHOUT ANY WARRANTY; without even the implied warranty of *)(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *)(* GNU Lesser General Public License for more details. *)(* *)(* See the GNU Lesser General Public License version 2.1 *)(* for more details (enclosed in the file LICENSE). *)(* *)(**************************************************************************)moduleGenericRelationalValued(Elt:Parameters.GENERIC_ELT)(Relation:Parameters.GENERIC_GROUP)(Value:Parameters.GENERIC_VALUEwithtype('a,'b)relation=('a,'b)Relation.t)=structmoduleEltSet=PatriciaTree.MakeHeterogeneousSet(Elt)(** Set of ['a Elt.t] *)(** {2 Existential wrappers for the return type of find operations} *)type'aelt_through_relation=EltThroughRel:'bElt.t*('a,'b)Relation.t->'aelt_through_relationtype'avalue_through_relation=ValueThroughRelation:'bValue.toption*('a,'b)Relation.t->'avalue_through_relationtype'aall_through_relation=AllThroughRelation:'bElt.t*'bValue.toption*EltSet.t*('a,'b)Relation.t->'aall_through_relationmoduleReprMap=PatriciaTree.MakeHeterogeneousMap(Elt)(structtype('a,_)t='aelt_through_relationend)(** Map [_ ReprMap.t] mapping ['a Elt.t] to ['a elt_through_relation] *)moduleClassMap=PatriciaTree.MakeHeterogeneousMap(Elt)(PatriciaTree.WrappedHomogeneousValue)(** Map ['b ClassMap.t] mapping ['a Elt.t] to ['b] *)moduleValueMap=PatriciaTree.MakeHeterogeneousMap(Elt)(structtype('a,_)t='aValue.tend)(** Union-find structure
Invariants on t:
- Flat union find: i.e. forall (k -> v) in representatives,
v is a reprensentative meaning:
- v in dom(classes) union dom(values)
- k in classes.find v
- classes are correct: forall (r -> c) in classes, forall x in c, repr(x) = r
- dom(representatives) is (almost) minimal:
- it contains all non-representatives
- it contains all non-trivial representatives (required for join), i.e:
- representatives x of non-singleton classes (x in dom(classes))
- representatives x with attached values (x in dom(values))
The relation for all non trivial representatives is [Relation.identity]
- it contains NO trivial representative
- classes is minimal: if v is a singleton class, v does not appear in classes
- Sorted union find: forall (k -> v) in representatives, v <= k.
or, more specifically, Elt.to_int v <= Elt.to_int k
*)typet={representatives:unitReprMap.t;(** map: ['a Elt.t --> ('b Elt.t * ('a, 'b) relation)],
mapping elements to representatives
representatives do not appear in this map's domain. *)classes:EltSet.tClassMap.t;(** map: ['a Elt.t --> EltSet.t,] mapping representatives to their classes
only non-singleton representatives appear in this map's domain *)values:unitValueMap.t;(** map: ['a Elt.t --> 'a Value.t] mapping representatives to their values
can contain singleton representatives *)}let(**)=Relation.composelet(~~)=Relation.inverselet(|>>)=Value.apply(** [are_sorted a b = true] IFF [a] is the chosen representative for [{a,b}] *)letare_sortedab=Int.compare(Elt.to_inta)(Elt.to_intb)<=0letequalab=ReprMap.reflexive_same_domain_for_all2{f=fun_(EltThroughRel(a,ra))(EltThroughRel(b,rb))->matchElt.polyeqabwith|Eq->Relation.equalrarb|Diff->false}a.representativesb.representatives(* By invariants, equality of representatives implies equality of classes *)(* &&
ClassMap.reflexive_same_domain_for_all2 a.classes b.classes
{ f = fun _ (V a) (V b) -> EltSet.equal a b } *)&&ValueMap.reflexive_same_domain_for_all2{f=fun_v1v2->Value.equalv1v2}a.valuesb.valuesletempty={representatives=ReprMap.empty;classes=ClassMap.empty;values=ValueMap.empty;}(** {2 Find operation} *)letfind_reprufx=matchReprMap.findxufwith|y->y(* by invariant, we don't need to search further *)|exceptionNot_found->EltThroughRel(x,Relation.identity)letfind_representativeufx=find_repruf.representativesxletfind_classufx=let(EltThroughRel(x,_))=find_repruf.representativesxinmatchClassMap.findxuf.classeswith|Sndy->y(* find_repr uf y (by invariant, we don't need to search further) *)|exceptionNot_found->EltSet.singletonxletfind_valueufx=let(EltThroughRel(x,rel))=find_repruf.representativesxinmatchValueMap.findxuf.valueswith|y->ValueThroughRelation(Somey,rel)|exceptionNot_found->ValueThroughRelation(None,rel)letfindufx=let(EltThroughRel(x,rel))=find_repruf.representativesxinletclasse=matchClassMap.findxuf.classeswith|Sndy->y(* find_repr uf y (by invariant, we don't need to search further) *)|exceptionNot_found->EltSet.singletonxinmatchValueMap.findxuf.valueswith|y->AllThroughRelation(x,Somey,classe,rel)|exceptionNot_found->AllThroughRelation(x,None,classe,rel)(** {2 Printers} *)moduleCMapForeign=ClassMap.WithForeign(ValueMap)(** Add s -> {s} to classes for all s appearing in values
This enables iterating over all classes *)letadd_singleton_classesuf=CMapForeign.update_multiple_from_foreignuf.values{f=funtx_->matchxwith|Some_->x|None->Some(Snd(EltSet.singletont))}uf.classesletpretty_classreprsfmtclasse=EltSet.pretty~pp_sep:(funfmt()->Format.fprintffmt";@ "){f=(funfmtx->letEltThroughRel(_,rel)=find_reprreprsxinFormat.fprintffmt"@[%a (via @[%a@])@]"Elt.prettyxRelation.prettyrel)}fmtclasseletpretty_class_and_repruffmtreprclasse=matchValueMap.find_optrepruf.valueswith|Somev->Format.fprintffmt"@[%a: (@[%a@]) {@[<hv>%a@]}@]"Elt.prettyreprValue.prettyv(pretty_classuf.representatives)classe|None->Format.fprintffmt"@[%a: (top) [@[<hv>%a@]}@]"Elt.prettyrepr(pretty_classuf.representatives)classeletpretty_polyprettyuf:EltSet.tClassMap.polypretty={f=(funfmtrepr(Sndclasse)->pretty_class_and_repruffmtreprclasse)}letprettyfmtuf=ifReprMap.is_emptyuf.representativesthenFormat.fprintffmt"Empty"elseFormat.fprintffmt"@[%a@]"(ClassMap.pretty~pp_sep:(funfmt()->Format.fprintffmt",@ ")(pretty_polyprettyuf))uf.classesletpretty_debugfmtuf=ifReprMap.is_emptyuf.representativesthenFormat.fprintffmt"Empty"elseFormat.fprintffmt"@[<v>Reprs: @[%a@]@ Classes: @[<v>%a@]@ Values: @[<v>%a@]@]"(ReprMap.pretty~pp_sep:Format.pp_print_space{f=funfmtk(EltThroughRel(v,r))->Format.fprintffmt"@[@[%a@]--(@[%a@])->@[%a@]@]"Elt.prettykRelation.prettyrElt.prettyv})uf.representatives(ClassMap.pretty~pp_sep:Format.pp_print_space{f=funfmtk(Sndc)->Format.fprintffmt"@[%a->{%a}@]"Elt.prettyk(EltSet.pretty~pp_sep:(funfmt()->Format.fprintffmt",@ "){f=Elt.pretty})c})uf.classes(ValueMap.pretty{f=funfmtvvalue->Format.fprintffmt"@[%a->%a@]"Elt.prettyvValue.prettyvalue})uf.values(** {2 Misc functions} *)(** Same as [check_related], but assumes [find] has already been performed
on the first argument *)letsemi_check_related(typeabc)uf(ra:cElt.t)(rel_a:(a,c)Relation.t)(b:bElt.t)=letEltThroughRel(rb,rel_b)=find_repruf.representativesbinmatchElt.polyeqrarbwith|Eq->Some(~~rel_b**rel_a)|Diff->Noneletcheck_relatedufab=letEltThroughRel(ra,rel_a)=find_repruf.representativesainsemi_check_relatedufrarel_ab(** Adds a value to the map, maintains the map small by removing top values *)letvalue_map_addvmapxvalue=ifValue.is_topvaluethenValueMap.removexvmapelseValueMap.addxvaluevmapletadd_valueufav=letAllThroughRelation(repr,val_repr,_,rel)=findufainletv=v|>>relinmatchval_reprwith|None->ifValue.is_topvthenufelseletuf={ufwithvalues=ValueMap.addreprvuf.values}inifReprMap.memrepruf.representativesthenufelse(* The representative is no longer trivial, add it to the map *){ufwithrepresentatives=ReprMap.addrepr(EltThroughRel(repr,Relation.identity))uf.representatives}|Somev'->letv=Value.meetv'vinifv==v'thenuf(* XXX: change to Value.equal ? *)else{ufwithvalues=ValueMap.addreprvuf.values}(** {2 Union operation} *)moduleReprMapSet=ReprMap.WithForeign(EltSet.BaseMap)(** [make_union uf x y rel]: Performs a simple union
@assumes [x] and [y] are representatives,
@assumes that [are_sorted x y]
@returns the union-find obtained by adding [y -(rel)-> x] in [uf] *)letmake_union(typexy)uf(x:xElt.t)(y:yElt.t)(rel:(y,x)Relation.t)=letclass_y=matchClassMap.findyuf.classeswith|Sndb->b|exceptionNot_found->EltSet.singletonyin(* the new [representatives] field: [uf.representatives] updated so that
elements that pointed to [y] now point to [x] *)letrepresentatives=ReprMapSet.update_multiple_from_foreignclass_y{f=(fun(typea)(elt:aElt.t)repr():aelt_through_relationoption->matchreprwith|None->beginmatchElt.polyeqyeltwith|Eq->Some(EltThroughRel(x,rel))|Diff->failwith"Broken invariant: All elements of uf.class (save the representative) should appear in uf.representatives"end|Some(EltThroughRel(repr_elt,rel_elt))->matchElt.polyeqyrepr_eltwith|Diff->failwith"Broken invariant: All elements of 'uf.class y' should have 'y' as representative"|Eq->Some(EltThroughRel(x,rel**rel_elt)))}uf.representativesin(* the new [classes] field: remove [y]'s class and add its elements to [x]'s *)letclasses=ClassMap.removeyuf.classesinletclasses=ClassMap.insertx(function|Some(Sndclass_x)->Snd(EltSet.unionclass_xclass_y)(* x is already non-trivial*)|None->letclass_y=(EltSet.addxclass_y)inSndclass_y)classesinletrepresentatives=(* x is now non-trivial (its class must contain y > x) *)ReprMap.addx(EltThroughRel(x,Relation.identity))representativesin(* the new [value] field: remove [y]'s value and combine it to [x]'s *)letvalues=matchValueMap.findyuf.valueswith|exceptionNot_found->uf.values(* No info on y, no need to update *)|value_y->letvalues=ValueMap.removeyuf.valuesinmatchValueMap.findxvalueswith|exceptionNot_found->value_map_addvaluesx(value_y|>>rel)|value_x->value_map_addvaluesx(Value.meetvalue_x(value_y|>>rel))in{representatives;classes;values}(** Performs union of the classes of x and y
We obtain the relations between the representatives using the following diagram:
x --(rel)--> y
| |
rel_x rel_y
| |
V v
repr_x repr_y
=> repr_x --(inv rel_x; rel; rel_y)--> repr_y (where ";" is composition)
=> repr_y --(inv rel_y; inv rel; rel_x)--> repr_x
We do need to flip the arguments, as R.compose as the opposite argument order.
A good thing about generic type is that the typechecker ensure we generate a valid
relation here.*)letunionufxyrel=let(EltThroughRel(repr_x,rel_x))=find_repruf.representativesxinlet(EltThroughRel(repr_y,rel_y))=find_repruf.representativesyinmatchElt.polyeqrepr_xrepr_ywith|Eq->letold_rel=~~rel_y**rel_xinifRelation.equalrelold_relthenOkufelseErrorold_rel|Diff->Ok(ifare_sortedrepr_xrepr_ythenmake_unionufrepr_xrepr_y(rel_x**~~rel**~~rel_y)elsemake_unionufrepr_yrepr_x(rel_y**rel**~~rel_x))letjoinab=letrepr_to_clean=refEltSet.emptyin(* map : repr_a -> repr_b -> repr_of_intersection for memoization *)letnew_classes=refClassMap.emptyinlet[@inline]memoize_setrepr_arepr_bvalue=new_classes:=ClassMap.addrepr_a(Snd(matchClassMap.findrepr_a!new_classeswith|Sndb->ClassMap.addrepr_b(Sndvalue)b|exceptionNot_found->ClassMap.singletonrepr_b(Sndvalue)))!new_classesinlet[@inline]memoize_getrepr_arepr_b=letSndmap_a=ClassMap.findrepr_a!new_classesinletSndb=ClassMap.findrepr_bmap_ainbin(* The new classes of the join, initialized by the intersection of the old classes *)letclasses=ref(ClassMap.idempotent_inter_filter{f=fun(typea)(repr:aElt.t)(PatriciaTree.Sndc1)(PatriciaTree.Sndc2)->letintersection=EltSet.interc1c2inifEltSet.is_emptyintersection||Option.is_some(EltSet.is_singletonintersection)then(repr_to_clean:=EltSet.addrepr!repr_to_clean;None)else(memoize_setreprrepr(EltSet.Anyrepr);Some(PatriciaTree.Sndintersection))}a.classesb.classes)in(* The new values of the join, initialized by the join of the old values *)letvalues=ref(ValueMap.idempotent_inter_filter{f=funreprv1v2->letv=Value.joinv1v2inifValue.is_topvthen(repr_to_clean:=EltSet.addrepr!repr_to_clean;None)elseSomev}a.valuesb.values)in(* Iterate through the intersection of element->representatives
(skipping elements that point to the same representative) *)letrepresentatives=ReprMap.idempotent_inter_filter{f=funxrx_arx_b->letEltThroughRel(repr_a,rel_x_a)=rx_ainletEltThroughRel(repr_b,rel_x_b)=rx_bin(* match Elt.polyeq repr_a repr_b with
| Eq when Relation.equal rel_x_a rel_x_b -> Some rx_a
| _ -> *)matchmemoize_getrepr_arepr_bwith|EltSet.Anyrepr->beginmatchsemi_check_relatedarepr_arel_x_arepr,semi_check_relatedbrepr_brel_x_breprwith|Somerel_a,Somerel_bwhenRelation.equalrel_arel_b->Some(EltThroughRel(repr,rel_a))|Some_,Some_->(* We can't join different relation: remove x from the class *)classes:=ClassMap.updaterepr(function|None->failwith"element should be in class"|Some(Sndclasse)->letclasse=EltSet.removexclasseinifOption.is_some(EltSet.is_singletonclasse)then(repr_to_clean:=EltSet.addrepr!repr_to_clean;None)elseSome(Sndclasse))!classes;None|_->failwith"Broken memoisation: Some elements of a class have different representatives"end|exceptionNot_found->(* Unmemoized value: find the new class, intersection of the old classes *)letclass_a=find_classarepr_ainletclass_b=find_classbrepr_binletnew_class=EltSet.interclass_aclass_bin(* We know that x is the representative of the new class, since
[idempotent_inter_filter] iterates through keys in ascending order
and we haven't hit our memoization cache *)(* Compute the new value associated with new representative x *)lethas_value=matchValueMap.find_optrepr_aa.values,ValueMap.find_optrepr_bb.valueswith|Someva,Somevb->letv=Value.join(va|>>~~rel_x_a)(vb|>>~~rel_x_b)inifValue.is_topvthenfalseelse(values:=ValueMap.addxv!values;true)|_->false(* One of the values is top, so the join is top *)in(* Only add the class and new representative if not a singleton.
We also don't need to memoize if the class is a singleton, since
it will not show up again. *)lethas_class=Option.is_none(EltSet.is_singletonnew_class)inifhas_classthen(classes:=ClassMap.addx(Sndnew_class)!classes;memoize_setrepr_arepr_b(EltSet.Anyx););ifhas_class||has_valuethenSome(EltThroughRel(x,Relation.identity))elseNone(* Trivial representative *)}a.representativesb.representativesinletrepresentatives=ReprMapSet.update_multiple_from_inter_with_foreign!repr_to_clean{f=funkeyrel()->ifValueMap.memkey!values||ClassMap.memkey!classesthenSomerelelseNone}representativesin{classes=!classes;representatives;values=!values}(** There is probably a faster way to do this. *)letsubseteqab=equalb(joinab)(** Another implementation of join, much slower, but more robust
used to test the first one. *)letjoin_testab=letrepresentatives=refReprMap.emptyinletclasses=refClassMap.emptyinletvalues=refValueMap.emptyin(* Iterate on pairs of classes, to compute all possible intersections *)ClassMap.iter{f=fun_(Sndclass_a)->ClassMap.iter{f=fun_(Sndclass_b)->letinter=EltSet.interclass_aclass_binifEltSet.is_emptyinterthen()elseletAnyx=EltSet.unsigned_min_eltinterin(* Value of new representative *)letValueThroughRelation(va,rel_a)=find_valueaxinletValueThroughRelation(vb,rel_b)=find_valuebxinlethas_value=matchva,vbwith|Someva,Somevb->values:=value_map_add!valuesx(Value.join(va|>>~~rel_a)(vb|>>~~rel_b));true|_->falsein(* Class of new representative *)letinter=EltSet.filter{f=fun(typea)(y:aElt.t)->matchElt.polyeqxywith|Eq->true(* keep in class but don't add to representatives *)|Diff->matchcheck_relatedayx,check_relatedbyxwith|Somerel_a,Somerel_bwhenRelation.equalrel_arel_b->representatives:=ReprMap.addy(EltThroughRel(x,rel_a))!representatives;true|Some_,Some_->false|_->failwith"Broken invariant"}interinlethas_class=Option.is_none(EltSet.is_singletoninter)inifhas_classthen(classes:=ClassMap.addx(Sndinter)!classes);ifhas_class||has_valuethen(representatives:=ReprMap.addx(EltThroughRel(x,Relation.identity))!representatives;)}(add_singleton_classesb)}(add_singleton_classesa);{classes=!classes;representatives=!representatives;values=!values}letcheck_invariantsuf=(* Replace XX.for_all with XX.iter for an exhaustive list of all problems
This version only returns one counter example per invariant. *)leterrors=ref[]inletpretty_class=pretty_classuf.representativesin(* Checks [uf.representatives]:
- Non self pointing elements:
- must bind to a representative (which maps to a class which contains them)
- Self pointing elements:
- must be non-trivial representatives (in dom(uf.classes) of dom(uf.values))
- must point to themselves via [Relation.identity] *)let_=ReprMap.for_all{f=fun(typea)(k:aElt.t)(EltThroughRel(r,rel):aelt_through_relation)->matchElt.polyeqkrwith|Diff->letcheck1=matchClassMap.find_optruf.classeswith|None->letmessage=Format.asprintf"Representative contains @[%a -(%a)-> %a@] but %a has a singleton class"Elt.prettykRelation.prettyrelElt.prettyrElt.prettyrinerrors:=message::!errors;false|Some(Sndc)->ifEltSet.memkcthentrueelse(letmessage=Format.asprintf"Representative contains @[%a -(%a)-> %a@] but %a is not in the class of %a {@[%a@]}"Elt.prettykRelation.prettyrelElt.prettyrElt.prettykElt.prettyrpretty_classcinerrors:=message::!errors;false)inletcheck2=matchReprMap.find_optruf.representativeswith|None->true|Some(EltThroughRel(r',rel'))->ifElt.polyeqrr'<>Diffthentrueelse(letmessage=Format.asprintf"Representative is not flat: @[%a -(%a)-> %a -(%a)-> %a"Elt.prettykRelation.prettyrelElt.prettyrRelation.prettyrel'Elt.prettyr'inerrors:=message::!errors;false)incheck1&&check2|Eq->ifnot(Relation.equalrelRelation.identity)then(letmessage=Format.asprintf"Self pointing elt in representatives whose relation isn't identity: %a -(%a)-> %a"Elt.prettykRelation.prettyrelElt.prettyrinerrors:=message::!errors;false)elseifnot(ClassMap.memruf.classes||ValueMap.memruf.values)then(letmessage=Format.asprintf"Trivial repr in representatives: %a (self pointing elt not in dom(classes) or dom(values))"Elt.prettykinerrors:=message::!errors;false)elsetrue}uf.representativesin(* check [uf.classes]:
- all classe have cardinal >= 2
- all classe have their min element has representatives
- all classe contain their representatives
- all elements of a class point to the classe's representative *)let_=ClassMap.for_all{f=funk(Sndc)->letn=EltSet.cardinalcinletcheck1=ifn>=2thentrueelse(letmessage=Format.asprintf"The class of %a has size %d (<2) : %a"Elt.prettyknpretty_classcinerrors:=message::!errors;false)inletAnyx=EltSet.unsigned_min_eltcinletcheck2=ifElt.polyeqkx<>Diffthentrueelse(letmessage=Format.asprintf"The representative of {@[<hv>%a@]} isn't minimal: it is %a, it should be %a"pretty_classcElt.prettykElt.prettyxinerrors:=message::!errors;false)inletcheck3=ifEltSet.memkcthentrueelse(letmessage=Format.asprintf"The class of %a doesn't contain its representative: %a"Elt.prettykpretty_classcinerrors:=message::!errors;false)inletcheck4=EltSet.for_all{f=funelt->matchReprMap.find_opteltuf.representativeswith|Some(EltThroughRel(x,_))when(Elt.polyeqxk=Diff)->letmessage=Format.asprintf"The class of %a {@[<hv>%a@]} contains %a, which points to a different elt: %a"Elt.prettykpretty_classcElt.prettyeltElt.prettyxinerrors:=message::!errors;false|None->letmessage=Format.asprintf"The class of %a {@[<hv>%a@]} contains %a, which has no representative"Elt.prettykpretty_classcElt.prettyeltinerrors:=message::!errors;false|Some_->true}cincheck1&&check2&&check3&&check4}uf.classesin(* Check [uf.values]:
- all elements of dom(uf.values) are representative
- no value verifies [Value.is_top] *)let_=ValueMap.for_all{f=funkv->letcheck1=matchReprMap.findkuf.representativeswith|EltThroughRel(k',_)whenElt.polyeqkk'<>Diff->true|EltThroughRel(k',rel)->letmessage=Format.asprintf"%a->%a appears in values, but isn't a repr (%a-(%a)->%a in representatives)"Elt.prettykValue.prettyvElt.prettykRelation.prettyrelElt.prettyk'inerrors:=message::!errors;false|exceptionNot_found->letmessage=Format.asprintf"Missing non-trivial repr: %a->%a appears in values, but not in representatives"Elt.prettykValue.prettyvinerrors:=message::!errors;falseinletcheck2=ifnot(Value.is_topv)thentrueelseletmessage=Format.asprintf"Top in values: %a->%a"Elt.prettykValue.prettyvinerrors:=message::!errors;falseincheck1&&check2}uf.valuesinif!errors=[]thenOk()elseError("BROKEN INVARIANTS:\n- "^String.concat"\n- "(List.rev!errors))end