123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357typeposition=Line_start|Inlinetypekind=Line_comment|Block_comment|Annotationtypetrivia=Itemof{content:string;kind:kind}|Blank_linetypeentry={anchor:int;trivia:trivia;position:position}typeassociated={before:entrylist;within:entrylist;after:entrylist;}typet=(Ast.location,associated)Hashtbl.ttypecontext={mutablecomments:entrylist;mutableat_start_of_line:bool;mutableprev_token_end:int;mutablelocations:Ast.locationlist;}letmake()={comments=[];at_start_of_line=true;prev_token_end=0;locations=[]}letadd_entryctxentry=ctx.comments<-entry::ctx.commentsletreport_itemctxkindcontent=add_entryctx{anchor=ctx.prev_token_end;trivia=Item{content;kind};position=(ifctx.at_start_of_linethenLine_startelseInline);};ctx.at_start_of_line<-kind=Line_commentletreport_newlinectx=ifctx.at_start_of_linethenadd_entryctx{anchor=ctx.prev_token_end;trivia=Blank_line;position=Line_start;};ctx.at_start_of_line<-trueletreport_tokenctxpos=ctx.at_start_of_line<-false;ctx.prev_token_end<-posletwith_posctxinfodesc=ctx.locations<-info::ctx.locations;{Ast.desc;info}letdrop_in_rangesctxranges=matchrangeswith|[]->()|_->letranges=List.sort(fun(a,_)(b,_)->compare(a:int)b)rangesin(* [ctx.comments] is built in reverse (most recent first); [List.rev] puts
it back in lexing order. A stable sort by anchor then keeps the lexing
order of comments sharing an anchor (consecutive line comments anchor
at the same preceding token). Sweep the ascending comments alongside
the ascending ranges in one pass, dropping any comment whose anchor
lies in a deleted range [\[start, end)], and restore the reverse order
{!associate} expects. *)letcomments=List.stable_sort(funab->comparea.anchorb.anchor)(List.revctx.comments)inletrecsweepranges=function|[]->[]|(c:entry)::rest->(letrecskip=function|(_,e)::rswhene<=c.anchor->skiprs|rs->rsinletranges=skiprangesinmatchrangeswith|(s,_)::_whens<=c.anchor->sweeprangesrest|_->c::sweeprangesrest)inctx.comments<-List.rev(sweeprangescomments)letassociate?onlyctx=(* Only consider locations the caller will actually look up while printing
(when [only] is given). A comment otherwise risks being attached to a node
that the printer never emits trivia for — e.g. a struct-field label printed
via its [.desc] only — and would then be silently dropped. Restricting to
emitted locations makes every comment bubble up to a location that prints. *)letlocations=matchonlywith|None->ctx.locations|Someset->List.filter(funl->Hashtbl.memsetl)ctx.locationsinlettbl=Hashtbl.create(List.lengthlocations)inletcomments=List.revctx.commentsinletlocs=List.sort(funab->letc=comparea.Ast.loc_start.Lexing.pos_cnumb.Ast.loc_start.Lexing.pos_cnuminifc<>0thencelsecompareb.Ast.loc_end.Lexing.pos_cnuma.Ast.loc_end.Lexing.pos_cnum)locationsin(* Collapse identical spans: a single source range is often recorded by more
than one node (e.g. a [Get] instruction and the identifier it wraps both
span the same name), and the printer looks each up but, via [seen], only
the first carries the trivia. Two same-range entries would otherwise make
[process] treat one as the other's child, and the "steal the last child's
trailing comments" path then hands the parent the child's (empty) [after]
instead of computing the gap up to the next sibling — silently dropping a
trailing comment anchored just past the span. *)letlocs=letsameab=a.Ast.loc_start.Lexing.pos_cnum=b.Ast.loc_start.Lexing.pos_cnum&&a.Ast.loc_end.Lexing.pos_cnum=b.Ast.loc_end.Lexing.pos_cnumin(* Tail-recursive (accumulate then reverse): [a :: dedup rest] is not
tail-recursive (OCaml's tail-modulo-cons is opt-in, via [@tail_mod_cons]),
so it recurses to the list length. The native stack absorbs that, but the
much smaller wasm call stack overflows on a large module with many
recorded locations (e.g. formatting a big .wat in the editor). *)letrecdedupacc=function|a::(b::_asrest)whensameab->dedupaccrest|a::rest->dedup(a::acc)rest|[]->List.revaccindedup[]locsinletpos_of_entrye=e.anchorinletsplit_beforethresholdcomments=letrecauxacc=function|c::restwhenpos_of_entryc<threshold->aux(c::acc)rest|rest->(List.revacc,rest)inaux[]commentsin(* Trailing comments of a node: those anchored in [\[parent_end, upto)], where
[upto] is the next sibling's start (so a comment separated from the node by
a punctuation token — e.g. a list comma — still trails it rather than
leading the next sibling). An inline line comment ends the node's line and
is its trailing comment; a line-start comment or blank line begins the next
sibling and is left in place. *)letget_afterparent_end~uptocomments=letin_gapanchor=anchor>=parent_end&&anchor<uptoinletrecauxacc=function|({anchor;trivia=Item{kind=Line_comment;_};position=Inline}asc)::restwhenin_gapanchor->(List.rev(c::acc),rest)|({anchor;trivia=Item{kind=Line_comment;_};position=Line_start;}asc)::restwhenin_gapanchor->(List.revacc,c::rest)|({anchor;trivia=Item_;_}asc)::restwhenin_gapanchor->aux(c::acc)rest|({anchor;trivia=Blank_line;_}asc)::restwhenin_gapanchor->(List.revacc,c::rest)|l->(List.revacc,l)inaux[]commentsin(* Rebuild the nesting from the flat, sorted (start asc, end desc — i.e.
preorder) location list in one linear pass. [subtree_end.(i)] is the last
index whose node is contained in [arr.(i)]: the maximal run of nodes right
after [i] whose end does not exceed [arr.(i)]'s. That run is exactly the
prefix the old recursive [span] recomputed at every nesting level, which
made the whole pass O(n^2) in depth. A monotonic stack of still-open
ancestors yields it in O(n): a node's subtree ends at [i-1] the moment we
meet an [i] whose end exceeds it (a strictly greater end means [i] is not
contained, so [i] is a sibling/uncle); equal ends keep it open, since an
equal-end node with a later start nests inside — matching [is_child]'s
[<=]. Any node never popped spans to the end. *)letarr=Array.of_listlocsinletn=Array.lengtharrinletecnumi=arr.(i).Ast.loc_end.Lexing.pos_cnuminletscnumi=arr.(i).Ast.loc_start.Lexing.pos_cnuminletsubtree_end=Array.make(maxn1)0inletstack=ref[]infori=0ton-1doletrecpop=function|t::tlwhenecnumt<ecnumi->subtree_end.(t)<-i-1;poptl|s->sinstack:=i::pop!stackdone;List.iter(funt->subtree_end.(t)<-n-1)!stack;(* Partition the comments over that tree. Semantics unchanged from the old
[process]: [before] = comments anchored before the node; [within] = those
between its children and its own end; [after] = trailing comments up to the
next sibling's start ([upto]) — or, when the node's last flat descendant
([arr.(child_hi)]) ends exactly where the node does, that descendant's
[after] (the [steal] path, preserving shared-span comment attachment).
Comments thread left to right through [rem]; each index is visited once as
a node, so the pass is O(n + #comments). The sibling tail call recurses in
width, the child call in depth — the same recursion depth (tree height) as
the old code. *)(* [last_upto] is the [upto] a range's *last* node uses — the point up to which
it may still claim trailing comments. For a child range it is the enclosing
parent's end ([ecnum] of the node that spawned the recursion), so the last
child reaches across its separator (a block's [;]) to grab the comment
trailing the last statement, and a comment after a stack of co-terminating
closers bubbles out to the *outermost* of them (each inner one's window ends,
exclusive, exactly where the comment is anchored). At the top level there is
no enclosing node, so it falls back to [ecnum lo + 1] and a module-tail
comment stays in [leftover]. *)letrecprocess_range~last_uptolohicomments=iflo>hithencommentselseletchild_lo=lo+1andchild_hi=subtree_end.(lo)inletnext_sib=child_hi+1inletupto=ifnext_sib<=hithenscnumnext_sibelsematchlast_uptowithSomeu->u|None->ecnumlo+1inletbefore,rem1=split_before(scnumlo)commentsinletrem2=process_range~last_upto:(Some(ecnumlo))child_lochild_hirem1in(* Own-line comments still inside the node ([within]) render inside it; an
inline trailing comment has already been claimed above by the last child
(as its [after]). Comments past the node's end are its [after] region,
rendered on the far side of the closing delimiter. *)letwithin,rem3=split_before(ecnumlo)rem2inletsteal_candidate=ifchild_hi>=child_lo&&ecnumchild_hi=ecnumlothenSomearr.(child_hi)elseNoneinletfinal_after,rem4=matchsteal_candidatewith|Somelast_child->(matchHashtbl.find_opttbllast_childwith|Someassoc->letstolen=assoc.afterinHashtbl.replacetbllast_child{assocwithafter=[]};(* A co-terminating last child's own [after] window collapsed to
empty (its [upto] reached only to [ecnum lo]); scan on up to
this node's [upto] so its real trailing comments aren't
dropped. Whatever the child did drain is already out of the
stream, so [extra] can't double-count. *)letextra,rem4=get_after(ecnumlo)~uptorem3in(stolen@extra,rem4)|None->get_after(ecnumlo)~uptorem3)|None->get_after(ecnumlo)~uptorem3inHashtbl.addtblarr.(lo){before;within;after=final_after};process_range~last_uptonext_sibhirem4inletleftover=process_range~last_upto:None0(n-1)commentsin(* [leftover] holds comments that no location owns: trailing comments after
the last node, or — when the module has no locations at all (e.g. an empty
[(module)]) — the whole file. The caller prints them as tail trivia. *)(tbl,leftover)letdrop_trailing_blank_linesentries=letrecdrop=function|{trivia=Blank_line;_}::rest->droprest|rest->restinList.rev(drop(List.reventries))letdummy_assoc={before=[];within=[];after=[]}letget?collecttrivia~seenloc=(* A dry pass records every looked-up location into [collect]; the real pass
then restricts {!associate} to that set. *)(match(collect,loc)with|Someset,Someloc->Hashtbl.replacesetloc()|_->());matchlocwith|None->dummy_assoc|Someloc->(matchHashtbl.find_opttrivialocwith|None->dummy_assoc|Someassoc->ifHashtbl.memseenlocthendummy_assocelse(Hashtbl.addseenloc();assoc))(* Cross-format delimiter translation. *)typecomment_syntax={line:string;block_open:string;block_close:string;}letwax_syntax={line="//";block_open="/*";block_close="*/"}letwat_syntax={line=";;";block_open="(;";block_close=";)"}letreplace_all~sub~bys=letsl=String.lengthsubinifsl=0thenselseletn=String.lengthsinletbuf=Buffer.createninletrecauxi=ifi>=nthenBuffer.contentsbufelseifi+sl<=n&&String.subsisl=subthen(Buffer.add_stringbufby;aux(i+sl))else(Buffer.add_charbufs.[i];aux(i+1))inaux0letretarget_content~src~dstkindcontent=matchkindwith|Line_comment->ifString.starts_with~prefix:src.linecontentthendst.line^String.subcontent(String.lengthsrc.line)(String.lengthcontent-String.lengthsrc.line)elsecontent|Block_comment->content|>replace_all~sub:src.block_open~by:dst.block_open|>replace_all~sub:src.block_close~by:dst.block_close|Annotation->contentletretarget_entry~src~dste=matche.triviawith|Item{content;kind}->{ewithtrivia=Item{content=retarget_content~src~dstkindcontent;kind};}|Blank_line->eletretarget~src~dsttbltail=letconv=retarget_entry~src~dstinletconv_assoca={before=List.mapconva.before;within=List.mapconva.within;after=List.mapconva.after;}inlettbl'=Hashtbl.create(Hashtbl.lengthtbl)inHashtbl.iter(funkv->Hashtbl.addtbl'k(conv_assocv))tbl;(tbl',List.mapconvtail)