123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311open!CoremoduleToken=Common.TokenmoduleGrammar=Common.Grammar(* The idea behind this file is to implement a quickcheck test for the semantics of each
grammar constructor, as written down in src/grammar_module_types_intf.ml. However, even
though the specification there is written down in 'relational' form, like
{v
g ⇓ ts ⇒ v
v}
it is difficult to test such a form comprehensively. Instead, we operate on the parse
sequences themselves, defining what they should be compositionally.
The strength of the semantics tested here is, pragmatically, somewhere in-between
fully-ordered and fully-unordered. That is, we do test that e.g. wrapping a grammar in
a [Map] should not change the parse order, but we make no specific assertion for
[Append] or [Star] (except that the first parse is composed of first parses too);
instead ordering_tests.ml establishes that the parse ordering is stable and formally
describable.
Generating such testcases is a bit of a challenge. First, the [Grammar.t] type is
type-indexed and contains closures, making it difficult to write a fully general
generator. Fortunately, the closures are only executed at the very end and can't affect
parse structure. So the approach we take is a [Blueprint.t] type, which is like a
[Grammar.t] but with the types and closures stripped; it can be easily generated. Such
a [Blueprint.t] can then be converted to a [Parse.t Grammar.t] most of the time (unless
it is necessitated to be a [unit Grammar.t]), where [Parse.t] is the most general parse
tree type (kind of like a free monoid). As long as any [Grammar.t] has a [Blueprint.t],
we have not lost generality yet.
Second, verifying the parse sequence itself. We use intermediate types [Semantics.t]
and [Value_stream.t] to help express the desired assertion for each constructor at a
high level. Unfortunately the parse sequence can be infinite; so we try to verify as
much as we can, returning [`Inconclusive] if it didn't feel like we tested things to an
adequate degree. (Which is also why we put the validation code inside the generator).
And as for the testcase generator, we make sure that it tests something by having the
input stream generator follow the grammar most of the time, such that there is at least
one match.
Overall, (not counting auxiliary tests), this leads to only one quickcheck test, but a
very powerful one, as the proposition it tests, if true, inductively implies the
correctness of the implementation for any possible grammar and input.
*)moduleParse=struct(* Technically speaking this type could be even more general if it included unique
indices based on origin, but this should be good enough too. *)typet=|Unit|Return|Mapoft|Appendoft*t|Star_empty|Star_consoft*t|Captureofstring|Capture_alsoofstring*t[@@derivingcompare,sexp_of,variants]includefunctorComparable.Make_plainendmoduleBlueprint=structtypet=|Return|Mapoft|TerminalofToken.t|Captureoft|Capture_alsooft|Unionoftlist|Appendoft*t|Concat_unitoftlist|Staroft|Star_unitoft[@@derivingcompare,equal,quickcheck,sexp_of](* warning: one of the ways we could lose coverage is if the mapping here doesn't
describe a perfect correspondence. There is a quickcheck test below that checks that,
but it wouldn't catch if e.g. [Blueprint.t] didn't have the [Star_unit] constructor,
and we just mapped [Grammar.Star_unit] to [Blueprint.Star].
*)letrecof_grammar:typea.aGrammar.t->t=function|Return_->Return|Map(t,_)->Map(of_grammart)|Terminalt->Terminalt|Capture(t,_)->Capture(of_grammart)|Capture_also(t,_)->Capture_also(of_grammart)|Unionts->Union(List.mapts~f:of_grammar)|Append(tl,tr,_)->Append(of_grammartl,of_grammartr)|Concat_unitts->Concat_unit(List.mapts~f:of_grammar)|Star(t,_)->Star(of_grammart)|Star_unitt->Star_unit(of_grammart);;letrecto_grammar_unit:t->unitGrammar.t=function|Return->Return()|Mapt->Map(to_grammar_unitt,fun()->())|Terminalt->Terminalt|Capturet->Capture(to_grammar_unitt,fun_~pos:_~len:_->())|Capture_alsot->Capture_also(to_grammar_unitt,fun_~pos:_~len:_()->())|Unionts->Union(List.mapts~f:to_grammar_unit)|Append(tl,tr)->Append(to_grammar_unittl,to_grammar_unittr,fun()()->())|Concat_unitts->Concat_unit(List.mapts~f:to_grammar_unit)|Start->Star(to_grammar_unitt,{init=();f=(fun()()->())})|Star_unitt->Star_unit(to_grammar_unitt);;(* Roundtrips with [of_grammar] (asserted below). It tries to construct a
[Parse.t Grammar.t], unless it's forced to construct a [unit Grammar.t].
Unfortunately because of that it's a bit complicated, and I don't feel like thinking
about ways to simplify it, but the quickcheck below makes me confident that it is
correct.
*)letrecto_grammar:t->[`UnitofunitGrammar.t|`FullofParse.tGrammar.t]=function|Return->`Full(ReturnParse.return)|Mapt->(matchto_grammartwith|`Unitg->`Full(Map(g,fun()->Parse.mapParse.unit))|`Fullg->`Full(Map(g,Parse.map)))|Terminalt->`Unit(Terminalt)|Capturet->`Full(Capture(to_grammar_unitt,funinput~pos~len->Parse.capture(String.subinput~pos~len)))|Capture_alsot->(matchto_grammartwith|`Unitg->`Full(Capture_also(g,funinput~pos~len()->Parse.capture_also(String.subinput~pos~len)Parse.unit))|`Fullg->`Full(Capture_also(g,funinput~pos~lenv->Parse.capture_also(String.subinput~pos~len)v)))|Unionts->letgs=List.mapts~f:to_grammarin(matchList.mapgs~f:(function|`Fullg->Someg|`Unit_->None)|>Option.allwith|Somegs->`Full(Uniongs)|None->`Unit(Union(List.mapts~f:to_grammar_unit)))|Append(tl,tr)->`Full(matchto_grammartl,to_grammartrwith|`Unitgl,`Unitgr->Append(gl,gr,fun()()->Parse.appendParse.unitParse.unit)|`Fullgl,`Unitgr->Append(gl,gr,funv()->Parse.appendvParse.unit)|`Unitgl,`Fullgr->Append(gl,gr,fun()v->Parse.appendParse.unitv)|`Fullgl,`Fullgr->Append(gl,gr,Parse.append))|Concat_unitts->`Unit(Concat_unit(List.mapts~f:to_grammar_unit))|Start->`Full(matchto_grammartwith|`Unitg->Star(g,{init=Parse.star_empty;f=(funv()->Parse.star_consvParse.unit)})|`Fullg->Star(g,{init=Parse.star_empty;f=Parse.star_cons}))|Star_unitt->`Unit(Star_unit(to_grammar_unitt));;endlet%expect_test"blueprint unit roundtrip"=let%quick_testprop(original:Blueprint.t)=letroundtripped=Blueprint.to_grammar_unitoriginal|>Blueprint.of_grammarin[%test_result:Blueprint.t]roundtripped~expect:originalin[%expect{| |}];;let%expect_test"blueprint general roundtrip"=let%quick_testprop(original:Blueprint.t)=letroundtripped=matchBlueprint.to_grammaroriginalwith|`Unitg->Blueprint.of_grammarg|`Fullg->Blueprint.of_grammargin[%test_result:Blueprint.t]roundtripped~expect:originalin[%expect{| |}];;moduleSemantics=structtype'at=|Fixed:(Token.tlist->'alist)->'at(* The parse sequence is as returned *)|Mapped:'aGrammar.t*(Token.tlist->'a->'b)->'bt(* The parse sequence is the same as for the given grammar, but remapped *)|Union:'atlist->'at(* The parse sequence consists of the following parse sequences, interleaved *)|Partition:'aGrammar.t*'bGrammar.t*('a->'b->'c)->'ct(* The parse is a union of the cartesian products for each partition, interleaved *)letidg=Mapped(g,fun_v->v)letof_grammar:typea.aGrammar.t->at=function|Returnx->Fixed(function|[]->[x]|_::_->[])|Map(t,f)->Mapped(t,fun_v->fv)|Terminalterminal->Fixed(function|[token]->ifGrammar.matchesterminaltokenthen[()]else[]|[]|_::_::_->[])|Capture_also(t,f)->Mapped(t,funtokensv->f(Common.tokens_to_stringtokens)~pos:0~len:(List.lengthtokens)v)|Capture(t,f)->id(Capture_also(t,funinput~pos~len()->finput~pos~len))|Unionts->Union(List.mapts~f:id)|Append(tl,tr,f)->Partition(tl,tr,f)|Concat_unit[]->id(Return())|Concat_unit(t::ts)->id(Append(t,Concat_unitts,fun()()->()))|Star(t,{init;f})->Union[id(Returninit);Partition(Star(t,{init;f}),t,f)]|Star_unitt->id(Star(t,{init=();f=(fun()()->())}));;(* A Sequence.Let_syntax.map2 that does not hang when one of the sequences is infinite
and the other empty *)letsequence_map2lr~f=ifSequence.is_emptyl||Sequence.is_emptyrthenSequence.emptyelseSequence.cartesian_productlr|>Sequence.map~f:(fun(l,r)->flr);;letrecpartitions:'alist->('alist*'alist)list=function|[]->[[],[]]|x::xs->([],x::xs)::List.map(partitionsxs)~f:(fun(l,r)->x::l,r);;letrecto_value_stream:typea.Token.tlist->at->aValue_stream.t=funinput->function|Fixedf->Simple(finput|>Sequence.of_list)|Mapped(g,f)->Simple(Common.parseginput|>Sequence.map~f:(finput))|Unionts->Interleaved(List.concat_mapts~f:(funt->matchto_value_streaminputtwith|Simplet->[t]|Interleavedts->ts))|Partition(gl,gr,f)->Interleaved(List.map(partitionsinput)~f:(fun(l,r)->sequence_map2(Common.parsegll)(Common.parsegrr)~f));;endmoduleTestcase=structtypet={blueprint:Blueprint.t;input:Token.tlist;outcome:unitOr_error.t}[@@derivingsexp_of]letgenerator=letopenQuickcheck.Generator.Let_syntaxinlet%mapblueprint=[%quickcheck.generator:Blueprint.t]andwhich_input=Quickcheck.Generator.weighted_union[0.8,return`Matching;0.2,return`Any]inlet%bind.Optiongrammar=matchBlueprint.to_grammarblueprintwith|`Fullg->Someg|`Unit_->Noneinlet%map.Optioninput_gen=matchwhich_inputwith|`Matching->Common.matching_tokens_for_grammargrammar|`Any->Some[%quickcheck.generator:Token.tlist]inlet%map.Quickcheck.Generatorinput=input_geninletexpected=Semantics.of_grammargrammar|>Semantics.to_value_streaminputinletactual=Common.parsegrammarinputinlet%map.Optionoutcome=matchValue_stream.test(moduleParse)~actual~expected~cutoff:100with|`Ok->Some(Ok())|`Errorerr->Some(Errorerr)|`Inconclusive->Nonein{blueprint;input;outcome};;letquickcheck_generator=generator|>Quickcheck.Generator.filter_map~f:Fn.id|>Quickcheck.Generator.join|>Quickcheck.Generator.filter_map~f:Fn.id;;let(quickcheck_shrinker:tQuickcheck.Shrinker.t)=Quickcheck.Shrinker.empty()endlet%expect_test"Semantics"=let%quick_testprop({outcome;_}:Testcase.t)=Or_error.ok_exnoutcomein[%expect{| |}];;