123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354(* A document-IR pretty-printer. The imperative builder API (string, space,
box, …) records a tree of text and breaks; [run] then renders it with our
own layout algorithm, which gives genuine hard breaks, column awareness and
break coalescing we control — unlike a thin layer over OCaml's [Format]. *)typebreak_strength=Cut|Space|Newline|Blank_lineletstrength=functionCut->0|Space->1|Newline->2|Blank_line->3(* ===================================================================== *)(* Document IR + imperative builder + renderer *)(* ===================================================================== *)moduleDoc=structtypegkind=|GBox(* fill: each soft break wraps independently as needed *)|GHov(* same fill semantics, kept distinct for parity with Format *)|GHv(* all-or-nothing: whole group flat, or every soft break wraps *)|GV(* every soft break wraps *)|GH(* soft breaks never wrap (hard/blank still break) *)(* A break carries one [break_strength]: Cut/Space are soft (flatten in a
fitting group); Newline/Blank_line always break. *)typedoc=|Empty|Textofint*string(* display width, payload *)|Brkofbreak_strength|Catofdoc*doc|Nestofint*doc|Groupofgkind*doc|If_brokenofdoc(* Emit [doc] only when the enclosing group is laid out broken (not flat): a
trailing comma after the last element of a list that wraps. It never counts
toward the fit decision (a flat list has no trailing comma). *)typeframe={wrap:doc->doc;mutableitems:doclist(* reversed *)}typestate={fmt:Format.formatter;width:int;mutablestack:framelist;mutablehas_emitted:bool;(* content since last forced end-of-line *)mutablepending_eol:(unit->unit)option;mutableholding_eol:bool;}letcreatefmt~width={fmt;width;stack=[{wrap=(fund->d);items=[]}];has_emitted=false;pending_eol=None;holding_eol=false;}lettopst=List.hdst.stack(* Append, coalescing a break that immediately follows another break (within
this frame) into the stronger of the two — the analogue of the Format
engine's [register_break]. Cross-frame coalescing is the renderer's job. *)letappendstd=letfr=topstinmatch(d,fr.items)with|Brks2,Brks1::tl->fr.items<-Brk(ifstrengths2>=strengths1thens2elses1)::tl|_->fr.items<-d::fr.itemsletpushstwrap=st.stack<-{wrap;items=[]}::st.stackletpopst=matchst.stackwith|fr::rest->(* items are most-recent-first; fold_left rebuilds source order. *)letbody=List.fold_left(funaccd->Cat(d,acc))Emptyfr.itemsinst.stack<-rest;appendst(fr.wrapbody)|[]->assertfalse(* Drop a trailing break at this frame's head, so a deferred end-of-line
comment hugs the preceding token instead of being pushed past a break. *)letdrop_trailing_breakst=letfr=topstinmatchfr.itemswith|Brks::tl->fr.items<-tl;Somes|_->Noneletforce_eolst=matchst.pending_eolwith|None->()|Someemit->st.pending_eol<-None;letdropped=drop_trailing_breakstinemit();letnext_brk=matchdroppedwithSomeBlank_line->Blank_line|_->Newlineinappendst(Brknext_brk);st.has_emitted<-falseletdefer_eolstemit=force_eolst;st.pending_eol<-Someemitletwith_held_eolstf=letprev=st.holding_eolinst.holding_eol<-true;f();st.holding_eol<-prevlethas_pending_eolst=st.pending_eol<>Nonelettextstlens=ifnotst.holding_eolthenforce_eolst;st.has_emitted<-true;appendst(Text(len,s))letstringsts=textst(String.lengths)sletstring_asstlens=textstlensletspacest=ifst.has_emittedthenappendst(BrkSpace)letcutst=appendst(BrkCut)letnewlinest=appendst(BrkNewline)letblank_linest=appendst(BrkBlank_line)letindentstnf=pushst(fund->Nest(n,d));f();popstletif_brokenstf=pushst(fund->If_brokend);f();popstletgroup_wrapkindindentd=Group(kind,ifindent=0thendelseNest(indent,d))letscopedstkind~skip_space~indentf=ifnotst.holding_eolthenforce_eolst;(ifskip_spacethenletfr=topstinmatchfr.itemswithBrk(Cut|Space)::tl->fr.items<-tl|_->());pushst(group_wrapkindindent);f();popstletboxst~skip_space~indentf=scopedstGBox~skip_space~indentflethvboxst~skip_space~indentf=scopedstGHv~skip_space~indentflethovboxst~skip_space~indentf=scopedstGHov~skip_space~indentfletvboxst~skip_space~indentf=scopedstGV~skip_space~indentflethboxst~skip_spacef=scopedstGH~skip_space~indent:0f(* --- renderer --- *)typemode=Flat|Brkm|Fill(* Does the upcoming content fit in [avail] columns up to the next line break?
Trailing context is included (the worklist [rest] beyond the immediate
item), so a group is laid flat only when what follows it on the line also
fits — matching Format, not a local "does this group alone fit" check.
A break decides line-end based on the carried mode: a break in a breaking
context ([Brkm]/[Fill]) ends the line, so we have fit so far ([true]); a
break in [Flat] is the flat-spacing of a unit being measured. Groups are
always entered [Flat] — measured as atomic units — so a group that cannot
be flat (a hard break inside) forces the preceding break to break. *)letrecfitsavailitems=ifavail<0thenfalseelsematchitemswith|[]->true|(i,m,d)::rest->(matchdwith|Empty->fitsavailrest|Text(w,_)->fits(avail-w)rest|Cat(a,b)->fitsavail((i,m,a)::(i,m,b)::rest)|Nest(n,d)->fitsavail((i+n,m,d)::rest)|Group(_,d)->fitsavail((i,Flat,d)::rest)|If_broken_->fitsavailrest(* A trailing comma never counts toward the fit: a flat list omits it
outright, and when measured inside an already-broken list a break
has ended the line before reaching it. *)|Brkb->(matchmwith|Brkm|Fill->true|Flat->(matchbwith|Newline|Blank_line->false|Space->fits(avail-1)rest|Cut->fitsavailrest)))letrender~widthdoc=letb=Buffer.create4096inletcol=ref0inletemitted=reffalsein(* Cap how far breaks indent, so deeply nested code does not march off to
the right margin (the analogue of Format's [max_indent]); deeper levels
then pile up at the cap rather than each pushing further right. *)letmax_indent=max0(width-10)in(* A pending line break (indent + blank?) and/or a pending flat separator.
Deferred until the next text and coalesced — across group boundaries —
by max strength; a line break supersedes a flat separator. *)letpend_line=refNoneinletpend_flat=refNoneinletflush()=(match!pend_linewith|Some(ind,blank)->(* Suppress a leading break before any output, like [if started]. *)if!emittedthen(Buffer.add_charb'\n';ifblankthenBuffer.add_charb'\n';Buffer.add_stringb(String.makeind' ');col:=ind)|None->(match!pend_flatwith|Someswhenstrengths>=strengthSpace->Buffer.add_charb' ';incrcol|_->()));pend_line:=None;pend_flat:=Noneinletbreak_lineindblank=letind=minindmax_indentin(match!pend_linewith|Some(_,b0)->pend_line:=Some(ind,b0||blank)|None->pend_line:=Some(ind,blank));pend_flat:=Noneinletflat_seps=if!pend_line=Nonethenpend_flat:=Some(match!pend_flatwith|Somes0->ifstrengths>=strengths0thenselses0|None->s)inleteff_col()=match!pend_linewith|Some(ind,_)->ind|None->!col+if!pend_flat<>Nonethen1else0inletrecgo=function|[]->()(* drop any trailing pending break: no trailing whitespace *)|(i,m,d)::rest->(matchdwith|Empty->gorest|Text(w,s)->flush();Buffer.add_stringbs;emitted:=true;col:=!col+w;gorest|Cat(a,c)->go((i,m,a)::(i,m,c)::rest)|Nest(n,d)->go((i+n,m,d)::rest)|Group(k,d)->(* A box's break-indentation is measured from the column where the
box opens (Format semantics), not from the inherited nesting —
they differ when a box starts mid-line (e.g. a WAT s-expression
after its [(]). Rebase the group's indent to the open column. *)letbase=eff_col()inletm'=matchkwith|GV->Brkm|GH->Flat|GHv->iffits(width-base)((base,Flat,d)::rest)thenFlatelseBrkm|GBox|GHov->Fillingo((base,m',d)::rest)|If_brokend->(* Shown only when the surrounding all-or-nothing box broke
([Brkm]); a one-line ([Flat]) or greedily-packed ([Fill]) list
has no trailing comma. *)go(matchmwithBrkm->(i,m,d)::rest|_->rest)|Brkstr->(match(str,m)with|Newline,_->break_lineifalse|Blank_line,_->break_lineitrue|(Cut|Space),Flat->flat_sepstr|(Cut|Space),Brkm->break_lineifalse|(Cut|Space),Fill->(* If kept flat this separator itself occupies a column, so
the following content starts one column further right;
account for it or a unit that just fits would overflow. *)letsep=matchstrwithSpace->1|_->0iniffits(width-eff_col()-sep)restthenflat_sepstrelsebreak_lineifalse);gorest)ingo[(0,Brkm,doc)];Buffer.contentsbletfinishst=force_eolst;letbody=List.fold_left(funaccd->Cat(d,acc))Empty(topst).itemsinlets=render~width:st.widthbodyin(* Emit line by line through the formatter's own newline, so that when [run]
is called inside an enclosing Format box (e.g. an [@[<2>…@]] wrapping the
snippet), every line — not just the first — picks up that box's
indentation. A bare [pp_print_string] of the multi-line string would
leave the embedded newlines at column 0. *)matchString.split_on_char'\n'swith|[]->()|first::rest->Format.pp_print_stringst.fmtfirst;List.iter(funline->Format.pp_force_newlinest.fmt();Format.pp_print_stringst.fmtline)restend(* ===================================================================== *)(* Public API *)(* ===================================================================== *)typet=Doc.stateletindent=Doc.indentletstring=Doc.stringletstring_as=Doc.string_asletspacet()=Doc.spacetletcutt()=Doc.cuttletnewlinet()=Doc.newlinetletblank_linet()=Doc.blank_linetletdefer_eol=Doc.defer_eolletwith_held_eol=Doc.with_held_eollethas_pending_eol=Doc.has_pending_eolletif_broken=Doc.if_brokenletboxt?(skip_space=false)?(indent=0)f=Doc.boxt~skip_space~indentflethvboxt?(skip_space=false)?(indent=0)f=Doc.hvboxt~skip_space~indentflethboxt?(skip_space=false)f=Doc.hboxt~skip_spaceflethovboxt?(skip_space=false)?(indent=0)f=Doc.hovboxt~skip_space~indentfletvboxt?(skip_space=false)?(indent=0)f=Doc.vboxt~skip_space~indentfletrun?(width=78)fmtf=letc=Doc.createfmt~widthinfc;Doc.finishc