12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016openCommonmoduletypePRINTER=sigtypetvalempty:tval(<+>):t->t->tvalchar:char->tvalsubstring:string->int->int->tvalfill:int->char->tendmoduletypeSIG=sigtypetvalempty:tvalsubstring:string->int->int->tvalstring:string->tvalchar:char->tvalfill:int->char->tvalline:string->tvalcut:tvalspace:tvalnest:int->t->tvalnest_list:int->tlist->tvalnest_relative:int->t->tvalgroup:t->tvalgroup_list:tlist->tvalwrap_words:string->tvalfill_paragraph:string->tval(<+>):t->t->tvalchain:tlist->tvalchain_separated:tlist->t->tvallist_separated:t->tlist->tendmoduleText=structtypet=|Stringofstring*int*int|Fillofint*char|Charofcharletstringsil=assert(0<=i);assert(0<=l);assert(i+l<=String.lengths);String(s,i,l)letcharc=Charcletfillnc=Fill(n,c)letlength=function|String(_,_,l)->l|Fill(n,_)->n|Char_->1letapply(f1:string->int->int->'a)(f2:int->char->'a)(f3:char->'a):t->'a=function|String(s,i,l)->f1sil|Fill(n,c)->f2nc|Charc->f3cendmoduleLine=structtypet={s:string;i:int}letmakesi={s;i}lettext(l:t):string=l.sletlengthl=String.lengthl.sletindentl=l.iend(* Gammar
d ::= t* g* c* -- document
g ::= [| g* c* |] -- group, at least one LB, either direct or
-- indirect
c ::= l t* g* -- chunk
*)typechunk={line:Line.t;texts:Text.tlist;cgroups:grouplist}andgroup={len:int;groups:grouplist;chunks:chunklist}moduleChunk=structletline(c:chunk):Line.t=c.lineletgroups(c:chunk):grouplist=c.cgroupslettexts(c:chunk):Text.tlist=c.textsletmake(line:Line.t):chunk={line;texts=[];cgroups=[]}letadd_text(t:Text.t)(c:chunk):chunk=assert(c.cgroups=[]);{cwithtexts=t::c.texts}letadd_group(g:group)(c:chunk):chunk={cwithcgroups=g::c.cgroups}endmoduleGroup=structletlength(g:group):int=g.lenletempty={len=0;groups=[];chunks=[]}letgroups(g:group):grouplist=g.groupsletchunks(g:group):chunklist=g.chunksletadd_text(t:Text.t)(g:group):group=matchg.chunkswith|[]->assertfalse(* Illegal call *)|c::tl->{gwithlen=g.len+Text.lengtht;chunks=Chunk.add_texttc::tl}letadd_line(l:Line.t)(g:group):group={gwithlen=g.len+Line.lengthl;chunks=Chunk.makel::g.chunks}letadd_group(gi:group)(go:group):group=letlen=go.len+gi.leninmatchgo.chunkswith|[]->{gowithlen;groups=gi::go.groups}|c::cs->{gowithlen;chunks=Chunk.add_groupgic::cs}endmoduleBuffer=structtypet={gs:grouplist;l:int;(* length *)o:int(* open groups*)}letis_empty(b:t):bool=(b.o=0)letlength(b:t):int=b.lletcount(b:t):int=b.oletgroups(b:t):grouplist=b.gsletempty:t={gs=[];l=0;o=0;}letpush(g:group)(b:t):t={gs=g::b.gs;l=Group.lengthg+b.l;o=b.o+1}letadd_text(t:Text.t)(b:t):t=letopenTextinmatchb.gswith|[]->assertfalse(* Illegal call *)|g::tl->{bwithgs=Group.add_texttg::tl;l=b.l+lengtht}letadd_line(l:Line.t)(b:t):t=matchb.gswith|[]->assertfalse(* Illegal call *)|g::tl->{bwithgs=Group.add_linelg::tl;l=b.l+Line.lengthl}letopen_groups(n:int)(b:t):t=assert(0<=n);letrecogsngs=ifn=0thengselseGroup.empty::ogs(n-1)gsin{bwitho=b.o+n;gs=ogsnb.gs}letclose_groups(n:int)(b:t):t=assert(0<=n);assert(n<b.o);letrecclosengs=ifn=0thengselsematchgswith|gi::go::tl->close(n-1)(Group.add_groupgigo::tl)|_->assertfalse(* Illegal call: cannot close group unless there is
one group to which it can be added. *)in{bwitho=b.o-n;gs=closenb.gs}endmoduleState=structtypeindent={line_indent:int;(* Indent of the current line *)current_indent:int;(* Current indentation level *)pos:int(* Position on the current line *)}typegroups={oe:int;(* open effecitve groups *)oa:int;(* open active groups *)o_r:int;(* open groups to the right of the last open
group in buffer *)}typeparams={width:int;(* desired maximal line width *)ribbon:int;(* desired maximal ribbon width *)}typet={params:params;indent:indent;groups:groups;buffer:Buffer.t}letinit(i:int)(width:int)(ribbon:int):t={params={width;ribbon};indent={line_indent=i;current_indent=i;pos=i};groups={oe=0;oa=0;o_r=0};buffer=Buffer.empty}letnormal(st:t):bool=Buffer.is_emptyst.bufferletbuffering(st:t):bool=not(Buffer.is_emptyst.buffer)letposition(st:t):int=st.indent.posletcurrent_indent(st:t):int=st.indent.current_indentletrelative_position(st:t):int=st.indent.pos-st.indent.current_indentletfits_pos(p:int)(st:t):bool=(* Is position [p] allowed, i.e. is it within the line width and the
ribbon width? *)p<=st.params.width&&p-st.indent.line_indent<=st.params.ribbonletfits(len:int)(st:t):bool=(* Do [len] more characters after the buffer still fit on the line? *)fits_pos(st.indent.pos+(Buffer.lengthst.buffer)+len)stletbuffer_fits(st:t):bool=(* Does the buffer fit on the line? *)fits0stletincrement_position(i:int)(st:t):t={stwithindent={st.indentwithpos=st.indent.pos+i}}letadd_text(t:Text.t)(st:t):t={stwithbuffer=Buffer.add_texttst.buffer}letadd_line(alternative_text:string)(st:t):t=assert(bufferingst);assert(0<st.groups.oa);leto=Buffer.countst.bufferinletbuffer=ifst.groups.oa<=othenBuffer.close_groups(o-st.groups.oa)st.bufferelsest.bufferin{stwithbuffer=Buffer.open_groups(ifo<st.groups.oathenst.groups.oa-oelsest.groups.o_r)buffer|>(Buffer.add_line@@Line.makealternative_textst.indent.current_indent)}letnewline(indent:int)(st:t):t=assert(normalst);{stwithindent={st.indentwithpos=indent;line_indent=indent}}letactive_to_effective(st:t):t=(* Make all active groups effective *)assert(normalst);{stwithgroups={st.groupswithoa=0;oe=st.groups.oe+st.groups.oa}}letone_active_to_effective(st:t):t=(* Make one active group effective *)assert(0<st.groups.oa);{stwithgroups={st.groupswithoa=st.groups.oa-1;oe=st.groups.oe+1}}letright_to_active(st:t):t={stwithgroups={st.groupswithoa=st.groups.oa+st.groups.o_r;o_r=0}}letstart_buffering(s:string)(st:t):t=assert(normalst);{stwithbuffer=Buffer.(open_groupsst.groups.oast.buffer|>add_line(Line.makesst.indent.current_indent));groups={st.groupswitho_r=0}}letclear_buffer(st:t):t={stwithbuffer=Buffer.empty}letpush(g:group)(st:t):t={stwithbuffer=Buffer.pushgst.buffer}letopen_group(st:t):t={stwithgroups=ifst.groups.oa<Buffer.countst.bufferthen{st.groupswitho_r=st.groups.o_r+1}else{st.groupswithoa=st.groups.oa+1}}letclose_group(st:t):t={stwithgroups=if0<st.groups.o_rthen(assert(st.groups.oa<Buffer.countst.buffer);{st.groupswitho_r=st.groups.o_r-1})elseif0<st.groups.oathen{st.groupswithoa=st.groups.oa-1}else(assert(0<st.groups.oe);{st.groupswithoe=st.groups.oe-1})}letincrement_indent(i:int)(st:t)=letcurrent_indent=st.indent.current_indent+iinassert(0<=current_indent);{stwithindent={st.indentwithcurrent_indent}}endmodulePretty(P:PRINTER)=structtypestate=State.ttypeloop_state=|Moreof(unit->loop_state)|DoneofP.tletloop:loop_state->P.t=letrecdo_loopi=function|Donep->p|Moref->do_loop(i+1)(f())indo_loop0type'acont=(* A continuation function takes a value, a state and the cumulated
printing command and returns the remainder of the loop. *)'a->state->P.t->loop_statemoduleM=structtype'at=state->P.t(* printed up to now *)->'acont(* continuation *)->loop_stateletreturn(a:'a):'at=funstpk->kastplet(>>=)(m:'at)(f:'a->'bt):'bt=funstpk->mstp(funastp->More(fun_->fastpk))endtypet=unitM.tletempty:t=M.return()let(<+>)(m1:t)(m2:t):t=M.(m1>>=fun_->m2)letstate:stateM.t=funstpk->kststpletput(st:state):unitM.t=fun_pk->k()stpletupdate(f:state->state):t=funstpk->k()(fst)pletget_and_update(f:state->state):stateM.t=funstpk->kst(fst)pletrelative_position:intM.t=M.(state>>=funst->return@@State.relative_positionst)letout_string(s:string)(start:int)(len:int):t=funstpk->More(fun_->k()(State.increment_positionlenst)P.(p<+>substringsstartlen))letout_char(c:char):t=funstpk->More(fun_->k()(State.increment_position1st)P.(p<+>charc))letout_fill(n:int)(c:char):t=assert(0<=n);funstpk->More(fun_->k()(State.increment_positionnst)P.(p<+>fillnc))letout_text(t:Text.t):t=Text.applyout_stringout_fillout_chartletout_line(indent:int):t=funstpk->More(fun_->k()(State.newlineindentst)P.(p<+>char'\n'<+>fillindent' '))letprint_list(l:'alist)(f:'a->t):t=M.(List.fold_right(funapr->pr>>=fun_->fa)l(return()))letout_texts(l:Text.tlist):t=print_listlout_textletout_alternative_text(l:Line.t):t=lets=Line.textlinout_strings0(String.lengths)letline_normal(s:string):t=M.(state>>=funst->if0<st.groups.oa&&State.fits(String.lengths)stthenput(State.start_bufferingsst)elseput(State.active_to_effectivest)>>=fun_->out_linest.indent.current_indent)letrecflush_flatten_group(g:group):t=(* Print the group [g] flattened. *)letopenGroupinM.(flush_flatten_groups(groupsg)>>=fun_->flush_flatten_chunks(chunksg))andflush_flatten_groups(gs:grouplist):t=(* Print all groups in the list [gs] flattened. *)print_listgsflush_flatten_groupandflush_flatten_chunks(cs:chunklist):t=(* Print all chunks in the list [cs] flattened. *)print_listcsflush_flatten_chunkandflush_flatten_chunk(c:chunk):t=(* Print the chunk [c] flattened. *)M.(out_alternative_text(Chunk.linec)>>=fun_->out_texts(Chunk.textsc)>>=fun_->flush_flatten_groups(Chunk.groupsc))letflush_flatten:t=(* flush the complete buffer flattening it i.e. print all line breaks
with their corresponding alternative text. *)letopenMinstate>>=funst->print_list(Buffer.groupsst.buffer)flush_flatten_group>>=fun_->updateState.clear_bufferletrecflush_group(g:group):t=(* Flush the complete group [g]. If it fits on the line, then flush it
flattened. Otherwise print its break hints as line breaks. *)letopenMinstate>>=funst->ifState.fits(Group.lengthg)stthenflush_flatten_groupgelseflush_groupsg.groups>>=fun_->flush_chunksg.chunksandflush_chunk(c:chunk):t=letopenMinout_line(Line.indent(Chunk.linec))>>=fun_->out_textsc.texts>>=fun_->flush_groupsc.cgroupsandflush_groups(gs:grouplist):t=print_listgsflush_groupandflush_chunks(cs:chunklist):t=print_listcsflush_chunkletflush_incomplete(is_last:bool)(g:group):t=(* Flush an incomplete group from the buffer *)letopenMinupdate(funst->if0<st.groups.oathenState.one_active_to_effectivestelseifis_lastthenState.right_to_activestelsest)>>=fun_->flush_groupsg.groups>>=fun_->flush_chunksg.chunksletflush_effective:t=(* Flush open groups until buffer fits or is empty. *)letopenMinletrecflush(remaining_len:int)(is_last:bool)=function|[]->assertfalse(* Illegal call! *)|[g]->flush_incompleteis_lastg|g::gs->letlen=Group.lengthg+remaining_leninflushlenfalsegs>>=fun_->state>>=funst->ifState.fitslenstthenput(State.pushgst)elseflush_incompleteis_lastginstate>>=funst->put(State.clear_bufferst)>>=fun_->flush0true(Buffer.groupsst.buffer)lettext(t:Text.t):t=letopenMinstate>>=funst->ifState.normalstthenout_texttelseletst=State.add_texttstinifState.buffer_fitsstthenputstelseputst>>=fun_->flush_effectiveletsubstring(s:string)(start:int)(len:int):t=assert(0<=start);assert(start+len<=String.lengths);text(Text.stringsstartlen)letstring(s:string):t=substrings0(String.lengths)letchar(c:char):t=text(Text.charc)letfill(n:int)(c:char):t=assert(0<=n);text(Text.fillnc)letrecline(alternative_text:string):t=letopenMinstate>>=funst->ifState.normalstthenline_normalalternative_textelseif0<st.groups.oathen(* Still inside the active group *)letst=State.(right_to_active@@add_linealternative_textst)inifState.buffer_fitsstthenputstelseputst>>=fun_->flush_effectiveelse(* Outside the active group. *)put(State.right_to_activest)>>=fun_->flush_flatten>>=fun_->linealternative_textletcut:t=line""letspace:t=line" "letrecchain(l:tlist):t=letopenMinmatchlwith|[]->return()|hd::tl->hd>>=fun_->chaintlletlist_separated(sep:t)(lst:tlist):t=letrecchn=function|[]->empty|[p]->p|p::tl->p<+>sep<+>chntlinchnlstletchain_separated(lst:tlist)(sep:t):t=list_separatedseplstletgroup(m:t):t=letopenMinupdateState.open_group>>=fun_->m>>=funa->updateState.close_group>>=fun_->returnaletnest(i:int)(m:t):'t=letopenMinget_and_update(State.increment_indenti)>>=funst->letpos,ind=State.positionst,State.current_indentstinletm=ifpos=0(*pos < ind + i*)thenfill(ind+i-pos)' '>>=fun_->melseminm>>=funa->update(State.increment_indent(-i))>>=fun_->returnaletnest_list(i:int)(lst:tlist):t=nesti(chainlst)letnest_relative(i:int)(m:t):t=letopenMinrelative_position>>=funp->nest(i+p)mletgroup_list(lst:tlist):t=group@@chainlstletwrap_words(s:string):t=letopenMinletis_blankc=c=' 'andis_not_blankc=c<>' 'inletword_starti=String.findis_not_blankisandword_endi=String.findis_blankisandlen=String.lengthsinletrecfillp=leti=word_startpin(ifp<ithengroupspaceelsereturn())>>=fun_->ifi<lenthenletj=word_endiinsubstringsi(j-i)>>=fun_->filljelsereturn()infill0letfill_paragraph=wrap_wordsletrun(indent:int)(width:int)(ribbon:int)(p:unitM.t):P.t=loop@@M.(p>>=fun_->flush_flatten)(State.initindentwidthribbon)P.empty(fun()_p->Donep)end(*
------------
Module Tests
------------
*)modulePP=Pretty(Readable_printer)includePPletrun(indent:int)(width:int)(ribbon:int)(m:t):Readable_printer.R.t=PP.runindentwidthribbonm|>Readable_printer.readablemoduleR=Readable_printer.Rletcompare(r:R.t)(s:string):bool=letlen=String.lengthsinletreccompri=ifi=lenthentrueelseletmore=R.has_morerinifnotmorethenfalseelses.[i]=R.peekr&&comp(R.advancer)(i+1)incompr0letprint_readable(r:R.t):unit=letopenPrintfinletrecprintr=ifR.has_morerthen(printf"%c"(R.peekr);print(R.advancer))else()inprintrlettest(w:int)(pflag:bool)(pp:t)(expected:string):bool=letres=compare(run0wwpp)expectedinifpflagthenprint_readable(run0wwpp);reslet%test_=letstr="01234 6789 0 2 4 6 8 01 34 6789 0"andres="01234 6789\n\
0 2 4 6 8\n\
01 34 6789\n\
0"intest10false(fill_paragraphstr)reslet%test_=test10false(nest_list4[string"0123";cut;string"456"]<+>cut<+>string"0123")" 0123\n\
\ 456\n\
0123"let%test_=compare(run02020(group_list[(group_list[string"class";nest_list4[space;string"Natural"];space;string"create"]);nest_list4[space;(group_list[string"0";line"; ";string"succ(Natural)"])];chain[space;string"end"]]))"class Natural create\n\
\ 0; succ(Natural)\n\
end"letmaybe=group(group(string"class"<+>nest4(space<+>string"Maybe(A)")<+>space<+>string"create")<+>nest4(space<+>group(string"nothing"<+>line"; "<+>string"just(A)"))<+>space<+>string"end")let%test_=compare(run07070maybe)"class Maybe(A) create nothing; just(A) end"let%test_=compare(run02020maybe)"class\
\n Maybe(A)\n\
create\
\n nothing; just(A)\n\
end"let%test_=compare(run01515maybe)"class\
\n Maybe(A)\n\
create\
\n nothing\
\n just(A)\n\
end"letplus=letgnsp=group(nest2(space<+>p))inletinsp=group(string"inspect"<+>nest2(space<+>group(string"a"<+>line"; "<+>string"(_:Natural) := Natural"))<+>space<+>string"case")andcases=group(string"0 :="<+>gns(string"b")<+>line"; "<+>string"n.successor :="<+>gns(string"n + b.successor"))instring"(+)(a:Natural,b:Natural): Natural :="<+>gns(group(insp<+>nest2(space<+>cases)<+>space<+>string"end"))let%test_=compare(run04040plus)"(+)(a:Natural,b:Natural): Natural :=\
\n inspect a; (_:Natural) := Natural case\
\n 0 := b\n n.successor := n + b.successor\
\n end"let%test_=compare(run03939plus)"(+)(a:Natural,b:Natural): Natural :=\
\n inspect\
\n a; (_:Natural) := Natural\
\n case\
\n 0 := b\
\n n.successor := n + b.successor\
\n end"let%test_=compare(run03333plus)"(+)(a:Natural,b:Natural): Natural :=\
\n inspect\n a; (_:Natural) := Natural\
\n case\
\n 0 := b\
\n n.successor :=\
\n n + b.successor\
\n end"