1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003(* Note [Basic Definitions]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The document structure is represented by a sequence of
- T: Text block (not empty)
- B: Break hint
- '[' Start group
- ']' End group
- '<' Start of indentation
_ '>' End of indentation
which is properly nested.
Example:
T T B [T B < T > B < [ T T T B] > ]
Break hints directly belonging to a group are either all effective or all
flattened. If the break hints of a group are flattened then all break hints
in the inner groups are flattened as well. The reverse is not true. In inner
group can be flattened and an outer group can be effective.
An indentation becomes effective after the next effective break. A text
block immediately following an effective break must be indented before
printing.
Indentation are best represented by a cumulated indentation i.e. for each
text block it has to be clear to which indentation level it belongs.
Having a decision for each group (effective or flattened) the layout is
fixed and can be printed.
*)(* Note [Transformation]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the following we assume that for each text block the cumulated
indentation level is known and therefore ignore the indentations.
The following transformations do not change the layout:
- A text block after the beginning of a group can be put before the group
and a text block after the end of the group can be moved inside the group
[ T ... ] ~> T [ ... ]
[ ... ] T ~> [ ... T ]
Reason: All break remain in the same group, flattening decisions are not
affected.
The document
T T B [T [B] T B [ T T T B] ]
can be transformed to
T T B T [[B T] B T T T [B] ]
After the transformation each group starts with a break or a complete group.
*)(* Note [Normalized Document]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A document is a string of zero or more
- B: break hint
- T: text block
- '[‘: start group
- ']‘: end group
where all groups are properly nested and for each text block the indentation
level is known.
We put the whole document in a group i.e. doc = [ ... ].
We get the normalized document by applying the above transformation as often
as possible (pull out a text block from after the start of a group, push a
text block immediately follwing a group into the group).
The normalized document has the structure:
doc ::= T* group
group ::= [ content ]
content ::= group* chunk*
chunk ::= B T* group*
Reason:
- A sequence of text blocks can be pulled out and put in front of the
the outermost group.
- Each group starts either with an inner group or a break hint.
- A chunk is a break hint followed by zero or more text blocks and zero
or more groups. The text blocks of a chunk are pulled out from the
first group of the chunk.
Each normalized document has an outermost group (possibly empty).
The outermost group is always effective.
*)(* Note [Break Decisions]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Basics
======================================================================
Pretty printing is scanning the document from left to right and decide for
each group if its direct break hints are effective or flattened. A
flattening decision is applied also to all inner groups.
We separate the document into 3 parts:
doc = head; buf; rest
where all break hints in head are decided, buf contains only undecided
break hints and rest is the remainder of the document.
Initially head and buf are empty and rest contains the complete document.
A buffer is a list of incomplete groups i.e. we have the following grammar
buf ::= ([ content)*
group ::= [ content ]
content ::= group* chunk*
chunk ::= B T* group*
doc ::= T* group
The buffer has a certain character length. The buffer is either empty or its
length satisfies the invariants
pos + |buf] <= width
|buf| <= ribbon
where pos is the position on the line after printing the head (including the
indentiation if the last item in the head is an effective break).
Scanning the document left to right triggers the following actions:
If the item is at the top level or belongs to an effective group, then the
buffer is empty and the item is appended to the head.
Filling the buffer
======================================================================
If the item belongs to a not yet decided group, then it has to be added to
the buffer.
If the buffer is empty, then the item is a start of a group followed by another
group or a break hint. In this case item starts the first incomplete group
in the buffer.
Now we consider the cases for non empty buffers:
1. The item is the start of a group. A new incomplete inner group is added
to the buffer.
2. The item is a break hint. A new chunk is appended to the chunks of the
innermost incomplete group.
3. The item is an end of group. This event closes the innermost incomplete
group from "[ group* chunk*" to "[ group* chunk* ]" and the group is
therefore no longer incomplete (form "[ group* chunk*").
3.1 The buffer has only one incomplete group: Since the buffer satisfies
the invariant, the now complete group fits on the current line and can
be appended as flattened group to the head and the buffer becomes empty.
3.2 The buffer has at least two incomplete groups: The situation in the
buffer looks like
[ ... [ group* chunk* [ group* chunk*
The last incomplete group becomes closed and is appended as a complete
group either to group* if there are no chunks in the outer group or to
the last chunk in chunk*.
4. The item is a text block. This innermost group has the form
[ group* chunk*
where a chunk has the general form "B T* group*". However a complete
group cannot be followed by a text block because of normalisation
(pushing text blocks following a group into the group). Therefore there
is a least one chunk in the incomplete group and the chunk does not end
with a complete group.
The text block is appended to the last chunk of the incomplete group.
Decisions
======================================================================
The adding of a text block or a break hint to the buffer might violate the
invariant (cases 2 and 4). This is fixed by pulling out incomplete groups
from the buffer until the invariant is satisfied or the buffer is empty.
All pulled out incomplete groups are decided to be effective and appended to
the head.
Note that appending incomplete groups to the head changes the position on
the current line.
*)openFmlib_std(* A Text Block
* ============
*
* A text block is never empty. It consists either of a (partial) string or an
* instruction to fill the output a character repeated.
*)moduleText=structtypet=|Stringofstring*int*int|Fillofint*charletsubstring(str:string)(start:int)(len:int):t=assert(0<=start);assert(0<len);(* Must not be empty, otherwise [peek] is not
possible. *)assert(start+len<=String.lengthstr);String(str,start,len)letstring(str:string):t=letlen=String.lengthstrinassert(0<len);(* Must not be empty, otherwise [peek] is not
possible. *)substringstr0lenletfill(n:int)(c:char):t=assert(0<n);Fill(n,c)letchar(c:char):t=Fill(1,c)letlength:t->int=function|String(_,_,len)->len|Fill(len,_)->lenletpeek(text:t):char=matchtextwith|String(s,start,_)->assert(start<String.lengths);s.[start]|Fill(_,c)->cletadvance(text:t):toption=matchtextwith|String(s,start,len)->if1<lenthenSome(String(s,start+1,len-1))elseNone|Fill(len,c)->if1<lenthenSome(Fill(len-1,c))elseNoneletto_string(text:t):string=matchtextwith|String(s,start,len)->String.subsstartlen|Fill(len,c)->String.makelenclet_=to_string(* might be needed during debugging *)end(* Line Formatting Data
* ====================
*)moduleLine=structtypet={indent:int;width:int;ribbon:int;}letmake(indent:int)(width:int)(ribbon:int):t={indent;width;ribbon;}letindent(l:t):int=l.indentletwidth(l:t):int=l.widthletribbon(l:t):int=l.ribbonletincrement_indent(i:int)(l:t):t=assert(0<=l.indent+i);{lwithindent=l.indent+i}letset_width(width:int)(l:t):t={lwithwidth}letset_ribbon(ribbon:int)(l:t):t={lwithribbon}end(* Groups and Chunks in the Buffer
* ===============================
*)typechunk={(*
chunk ::= B T* group*
*)break_text:string;line:Line.t;(* before the first text block. *)texts:Text.tDeque.t;chunk_groups:groupDeque.t;}andgroup={(*
group :: group* chunk*
*)glength:int;(* Total number of chars in the group whare all
breaks are flattened. *)complete_groups:groupDeque.t;chunks:chunkDeque.t;}moduleChunk=structtypet=chunkletmake(break_text:string)(line:Line.t):chunk={break_text;line;texts=Deque.empty;chunk_groups=Deque.empty;}letpush_text(text:Text.t)(chunk:chunk):chunk=(* If the chunk has already groups, no more text can be added. *)assert(Deque.is_emptychunk.chunk_groups);{chunkwithtexts=Deque.push_reartextchunk.texts}letupdate_line(line:Line.t)(chunk:chunk):chunk=assert(Deque.is_emptychunk.chunk_groups);ifDeque.is_emptychunk.textsthen{chunkwithline}elsechunkletadd_group(group:group)(chunk:chunk):chunk={chunkwithchunk_groups=Deque.push_reargroupchunk.chunk_groups;}letbreak_text(chunk:chunk):string=chunk.break_textletline(chunk:chunk):Line.t=chunk.linelettexts(chunk:chunk):Text.tDeque.t=chunk.textsletgroups(chunk:chunk):groupDeque.t=chunk.chunk_groupsendmoduleGroup=structtypet=groupletempty:t={glength=0;complete_groups=Deque.empty;chunks=Deque.empty;}letpush_text(text:Text.t)(group:t):t=(* Text can only be pushed to a group if it has at least one chunk. *)assert(not(Deque.is_emptygroup.chunks));{groupwithchunks=Deque.update_last(Chunk.push_texttext)group.chunks;glength=group.glength+Text.lengthtext;}letpush_break(str:string)(line:Line.t)(group:t):t={groupwithchunks=Deque.push_rear(Chunk.makestrline)group.chunks;glength=group.glength+String.lengthstr;}letupdate_line(line:Line.t)(group:t):t=assert(not(Deque.is_emptygroup.chunks));{groupwithchunks=Deque.update_last(Chunk.update_lineline)group.chunks;}letadd_complete(complete_group:t)(group:t):t=ifDeque.is_emptygroup.chunksthen{groupwithcomplete_groups=Deque.push_rearcomplete_groupgroup.complete_groups;}else{groupwithchunks=Deque.update_last(Chunk.add_groupcomplete_group)group.chunks;}letlength(group:t):int=group.glengthletcomplete_groups(group:t):tDeque.t=group.complete_groupsletchunks(group:t):chunkDeque.t=group.chunksend(* Buffer
* ======
*)moduleBuffer=structtypet={ngroups:int;length:int;(* Number of characters in the buffer. *)groups:grouplist;(* Reversed order, topmost is innermost. *)}letempty:t={ngroups=0;length=0;groups=[];}letcount(b:t):int=b.ngroupsletis_empty(b:t):bool=b.ngroups=0letlength(b:t):int=b.lengthletpush_text(text:Text.t)(buffer:t):t=assert(0<countbuffer);matchbuffer.groupswith|[]->assertfalse(* illegal call! *)|hd::tl->{bufferwithgroups=(Group.push_texttexthd)::tl;length=buffer.length+Text.lengthtext;}letpush_break(str:string)(line:Line.t)(b:t):t=assert(0<countb);matchb.groupswith|[]->assertfalse(* illegal call! *)|hd::tl->{bwithgroups=(Group.push_breakstrlinehd)::tl;length=b.length+String.lengthstr}letclose_one(buffer:t):t=matchbuffer.groupswith|last::previous::groups->{bufferwithgroups=Group.add_completelastprevious::groups;ngroups=buffer.ngroups-1;}|_->assertfalse(* Illegal call! *)letclose_and_open(nclose:int)(nopen:int)(str:string)(line:Line.t)(buffer:t):t=assert(nclose=0||nclose<countbuffer);assert(0<countbuffer+nopen-nclose);push_breakstrline{(Int.iteratencloseclose_onebuffer)withngroups=buffer.ngroups+nopen;groups=Int.iteratenopen(fungs->Group.empty::gs)buffer.groups;}letupdate_line(line:Line.t)(buffer:t):t=matchbuffer.groupswith|[]->buffer|group::groups->{bufferwithgroups=Group.update_linelinegroup::groups;}letreverse(b:t):t={bwithgroups=List.revb.groups}letpop(b:t):(Group.t*t)option=matchb.groupswith|[]->None|g::groups->letlen=Group.lengthginassert(len<=b.length);Some(g,{ngroups=b.ngroups-1;length=b.length-len;groups})end(* Pretty Printer State
* ====================
*)typet={user_line:Line.t;(* Current line data from the user *)next_line:Line.t;line:Line.t;(* This line *)position:int;(* Current position on the line *)active_groups:int;(* Number of open active groups *)effective_groups:int;(* Number of open effective groups *)right_groups:int;(* Number of open groups to the right of the last
open group in the buffer *)buffer:Buffer.t;}letinit(width:int)(ribbon:int):t=letline=Line.make0widthribbonin{position=0;user_line=line;next_line=line;line;active_groups=0;effective_groups=0;right_groups=0;buffer=Buffer.empty}letline_indent(s:t):int=ifs.position=0thenLine.indents.lineelse0letadvance_position(n:int)(s:t):t=assert(s.position<>0||Line.indents.line<=n);(* Positions between 0 and line_indent are illegal. *){swithposition=s.position+n}letnewline(s:t):t={swithposition=0;line=s.next_line;}letnewline_with_line(line:Line.t)(s:t):t={swithposition=0;line;next_line=line;}letfits(n:int)(s:t):bool=(* Is it possible to put [n] more characters on the current line without
* violating the line width and the ribbon size? *)ifs.position=0thenn<=Line.ribbons.line&&Line.indents.line+n<=Line.widths.lineelsebeginassert(Line.indents.line<=s.position);s.position-Line.indents.line+n<=Line.ribbons.line&&s.position+n<=Line.widths.lineend(* Groups and Buffering
* ====================
*)letbuffered_groups(s:t):int=(* Number of open groups in the buffer. *)Buffer.counts.bufferletis_buffering(s:t):bool=0<buffered_groupssletdirect_out(s:t):bool=not(is_bufferings)letline_direct_out(s:t):bool=direct_outs&&s.active_groups=0letwithin_active(s:t):bool=0<s.active_groupsletbuffer_fits(s:t):bool=fits(Buffer.lengths.buffer)sletpush_text(text:Text.t)(s:t):t=assert(is_bufferings);{swithbuffer=Buffer.push_texttexts.buffer}letpush_break(str:string)(s:t):t=(* Push a break hint to the buffer. At the end, the number of incomplete
* groups in the buffer and the number of active groups must be the same
* and the number of right groups must be zero. *)assert(within_actives);letoa=s.active_groupsandnbuf=buffered_groupssinletnclose,nopen=ifnbuf=0then(* Start buffering *)0,oaelseifoa<=nbufthen(* The innermost [nbuf - oa] groups in the buffer have already been
* closed by the user. We close these groups in the buffer as well
* and open [right_groups] in the buffer there the last group has a
* chunk with the break hint. *)nbuf-oa,s.right_groupselse(* nbuf < oa *)assertfalse(* This case cannot happen. At the start of buffering
we have [nbuf = oa]. If more groups are opened, they
are all counted as [right_groups]. *)in{swithright_groups=0;active_groups=oa+s.right_groups;buffer=Buffer.close_and_opennclosenopenstrs.user_lines.buffer;}letpull_buffer(s:t):Buffer.t*t=Buffer.reverses.buffer,{swithbuffer=Buffer.empty}letflatten_done(s:t):t=(* The complete buffer has been flattened. *)assert(not(is_bufferings));assert(s.active_groups=0);{swithactive_groups=s.right_groups;right_groups=0;next_line=s.user_line}leteffective_done(buffer:Buffer.t)(nflushed:int)(s:t):t=(* The outermost [nflushed] groups have been flushed as effective groups.
Now the buffer fits.
*)assert(not(is_bufferings));assert(Buffer.is_emptybuffer||fits(Buffer.lengthbuffer)s);letbuffer=Buffer.reversebufferandnflushed=minnflusheds.active_groups(* Of the flushed groups, only the groups
count which had been open initially. *)inleteffective_groups,active_groups,right_groups=ifBuffer.countbuffer=0then(* All groups have been flushed, buffer is empty. *)begins.effective_groups+nflushed,s.active_groups+s.right_groups-nflushed,0endelseif0<s.active_groupsthens.effective_groups+nflushed,s.active_groups-nflushed,s.right_groupselsebeginassert(s.active_groups=0);assert(nflushed=0);s.effective_groups,s.right_groups,0endin{swithbuffer;effective_groups;active_groups;right_groups;}(* Enter and Leave Groups
* ======================
*)letenter_group(s:t):t=ifis_bufferingsthen{swithright_groups=s.right_groups+1}else{swithactive_groups=s.active_groups+1}letleave_group(s:t):t=if0<s.right_groupsthen{swithright_groups=s.right_groups-1}elseif0<s.active_groupsthen{swithactive_groups=s.active_groups-1}elsebeginassert(0<s.effective_groups);{switheffective_groups=s.effective_groups-1}end(* Indenting and Line Info
* =======================
*)letupdate_line(new_line:Line.t)(s:t):t={swithuser_line=new_line;line=ifs.position=0then(* Nothing has been printed to the current line. The
* [line] has to be updated immediately. *)new_lineelses.line;next_line=ifdirect_outsthen(* In direct output mode the new indentation must become
* effective for the next line *)new_lineelses.next_line;buffer=Buffer.update_linenew_lines.buffer;}letincrement_indent(n:int)(s:t):t=assert(0<=Line.indents.user_line+n);update_line(Line.increment_indentns.user_line)sletwidth(width:int)(s:t):int*t=assert(0<=width);Line.widths.user_line,update_line(Line.set_widthwidths.user_line)sletribbon(ribbon:int)(s:t):int*t=assert(0<=ribbon);Line.ribbons.user_line,update_line(Line.set_ribbonribbons.user_line)s