123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856(* The package sedlex is released under the terms of an MIT-like license. *)(* See the attached LICENSE file. *)(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)(*
Implementation overview
=======================
Sedlex compiles regular expressions to Tagged DFAs.
1. NFA construction (type regexp = node -> node)
Each regexp combinator (chars, seq, alt, rep, ...) is a function that,
given a successor node, builds a fragment of NFA and returns its entry
node. This continuation-passing style makes sequencing natural (seq is
just function composition) and avoids explicit epsilon nodes for
concatenation.
2. Tags for `as` bindings (Laurikari-style)
NFA nodes may carry a tag operation (Set_position or Set_value).
[bind] wraps a sub-regexp with start/end tagged epsilon nodes so the
DFA can record sub-match positions at runtime. When the PPX can
compute one boundary from a known offset (see [pos_expr] in
ppx_sedlex.ml), [bind_start_only] or [bind_end_only] is used instead,
saving a memory cell. Discriminator tags (Set_value) disambiguate
or-patterns where multiple branches bind the same name.
3. Determinization (compile)
Subset construction for tagged NFAs (Laurikari, "NFAs with Tagged
Transitions", 2000), with the one-symbol lookahead of Trofimovich,
"Tagged Deterministic Finite Automata with Lookahead" (2017): a
TDFA(1).
- A DFA state is an ordered list of configurations (NFA node, tag ->
register map). One map per path, rather than one shared vector,
keeps captures correct when a tagged node is reachable from several
paths at once, e.g. a Star loop whose closure contains the start of
the following capture.
- Order is priority. The epsilon closure is a DFS along [eps] lists
and the first path to reach a node wins: leftmost-greedy
disambiguation among the parses of the longest match.
- Lookahead: the tag writes met in a state's closure are delayed. A
configuration performs them when it takes a character transition;
one that cannot consume the next character never does. So the end
of [Plus digit as n] is written once, on leaving the loop.
- Transition operations execute before their character is consumed,
final operations on entering the state.
- End of input is a transition of its own, read once: it leads to a
state without transitions. It does not advance the position but
counts as one more symbol read, so a match that reads it is longer
than the state's own accept.
- Each tag write of a transition gets a fresh register. States are
looked up modulo register renaming, with equal delayed writes;
reaching an existing state emits the moves realigning the
registers. The operations of a
transition form a parallel move, which the code generator
implements by let-binding the sources it overwrites.
- Canonical cells (cell = tag id) are what the generated bindings
read. Only the final operations of accepting states write them,
from the accepting path's registers and delayed writes, just
before [Sedlexing.mark]; working registers live above them. So the
cells of the marked rule survive a failed longer match, and
[Sedlexing.mark] and [Sedlexing.backtrack] save and restore nothing.
Future work (see #175): register optimization as in Trofimovich 2017
(section 7), DFA minimization.
*)moduleCset=Cset(* NFA *)typetag_op=|Set_positionof{dst:int}|Set_valueof{dst:int;value:int}|Copyof{dst:int;src:int}typenode={id:int;(** Unique identifier; nodes are compared by it. *)mutableeps:nodelist;(** Epsilon successors (no input consumed). *)mutabletrans:(Cset.t*node)list;(** Char-set-labelled transitions. *)tag:tag_opoption;(** Tag operation executed when entering this node. *)}(* Compilation regexp -> NFA *)typeregexp=node->nodeletcur_id=ref0letnew_node()=incrcur_id;{id=!cur_id;eps=[];trans=[];tag=None}letnew_tagged_nodetag_op=incrcur_id;{id=!cur_id;eps=[];trans=[];tag=Sometag_op}letseqr1r2succ=r1(r2succ)(* [is_chars final node] tests whether [node] is a simple character-set
node: no epsilon edges, a single transition to [final], and no tag.
Used by [alt] to merge adjacent character classes into a single [chars]
node instead of introducing an epsilon fork. *)letis_charsfinal=function|{eps=[];trans=[(c,f)];tag=None;_}whenf==final->Somec|_->Noneletcharscsucc=letn=new_node()inn.trans<-[(c,succ)];nletaltr1r2succ=letnr1=r1succandnr2=r2succinmatch(is_charssuccnr1,is_charssuccnr2)with|Somec1,Somec2->chars(Cset.unionc1c2)succ|_->letn=new_node()inn.eps<-[nr1;nr2];nletreprsucc=letn=new_node()inn.eps<-[rn;succ];n(* [r, Star r] with the body duplicated. A single loop entered at the body
would put the exit ahead of a second iteration when the first one
consumed nothing. *)letplusrsucc=r(reprsucc)letepssucc=succ(* eps for epsilon *)letrecrepeatrnmsucc=assert(0<=n&&n<=m);match(n,m)with|0,0->succ(* Taking an iteration comes first: repetition is greedy. *)|0,m->alt(funsucc->r(repeatr0(m-1)succ))epssucc|n,m->r(repeatr(n-1)(m-1)succ)letcomplr=letn=new_node()inmatchis_charsn(rn)with|Somec->Some(chars(Cset.differenceCset.anyc))|_->Noneletpair_opfr0r1=(* Construct subtract or intersection *)letn=new_node()inletto_charsr=is_charsn(rn)inmatch(to_charsr0,to_charsr1)with|Somec0,Somec1->Some(chars(fc0c1))|_->Noneletsubtract=pair_opCset.differenceletintersection=pair_opCset.intersection(* Tags for as-bindings *)letcur_tag=ref0letreset_tags()=cur_tag:=0letnew_tag()=lett=!cur_taginincrcur_tag;tletbindr=letstart_tag=new_tag()inletend_tag=new_tag()inletwrappedsucc=letend_node=new_tagged_node(Set_position{dst=end_tag})inend_node.eps<-[succ];letinner=rend_nodeinletstart_node=new_tagged_node(Set_position{dst=start_tag})instart_node.eps<-[inner];start_nodein(wrapped,start_tag,end_tag)letbind_start_onlyr=letstart_tag=new_tag()inletwrappedsucc=letinner=rsuccinletstart_node=new_tagged_node(Set_position{dst=start_tag})instart_node.eps<-[inner];start_nodein(wrapped,start_tag)letbind_end_onlyr=letend_tag=new_tag()inletwrappedsucc=letend_node=new_tagged_node(Set_position{dst=end_tag})inend_node.eps<-[succ];rend_nodein(wrapped,end_tag)letnew_disc_cell()=new_tag()letbind_discrcellvalue=letwrappedsucc=letdisc_node=new_tagged_node(Set_value{dst=cell;value})indisc_node.eps<-[succ];rdisc_nodeinwrapped(* One rule of a [match%sedlex]: the entry and final nodes of its NFA. *)typerule={entry:node;final:node}(* [compile_re re] instantiates a regexp by creating a fresh final node
and passing it as the successor. *)letcompile_re(re:regexp):rule=letfinal=new_node()in{entry=refinal;final}(* The compiled automaton *)typeaccept={rule:int;final_ops:tag_oplist}typedfa_state={trans:(Cset.t*int*tag_oplist)array;accept:acceptoption;}typedfa=dfa_statearraytypecompiled={dfa:dfa;num_tags:int}letop_dest=function|Copy{dst;_}|Set_position{dst}|Set_value{dst;_}->dst(* Determinization (tagged subset construction, see the overview above) *)(* A memory cell, by index. *)typecell=intmoduleTagMap=Map.Make(Int)moduleCellMap=Map.Make(Int)(* What a transition writes: the current position or a discriminator. *)typewrite=Position|Valueofintletop_of_write(dst:cell):write->tag_op=function|Position->Set_position{dst}|Valuevalue->Set_value{dst;value}(* Where a tag's value lives while a transition is computed: in a cell, or
in a write of that transition. Same tag and same write: same register. *)typeaddr=Cellofcell|Pendingofwrite(* One NFA path: the node it reached, where each tag it wrote lives, and the
closure's writes, [delayed] until it takes a character transition. *)type'aconfig={node:node;tags:'aTagMap.t;delayed:writeTagMap.t}(* A DFA state: configurations in priority order. A [candidate] comes out
of [eps_closure]; a [stored] state has every tag in a cell. *)type'astate='aconfiglisttypecandidate=addrstatetypestored=cellstate(* Only nodes with character transitions and final nodes belong in a
state; epsilon-only nodes would just bloat its key. *)letis_relevant(node:node):bool=node.trans<>[]||node.eps=[](* DFS along [eps] lists; the first path to reach a node wins, which is the
leftmost-greedy policy. Tag writes on the way are [delayed]. *)leteps_closure(seeds:addrconfiglist):candidate=letvisited=Hashtbl.create16inletacc=ref[]inletrecvisitnodetagsdelayed=ifnot(Hashtbl.memvisitednode.id)then(Hashtbl.addvisitednode.id();letdelayed=matchnode.tagwith|None->delayed|Some(Set_position{dst})->TagMap.adddstPositiondelayed|Some(Set_value{dst;value})->TagMap.adddst(Valuevalue)delayed|Some(Copy_)->assertfalse(* never carried by NFA nodes *)inifis_relevantnodethenacc:={node;tags;delayed}::!acc;List.iter(funn->visitntagsdelayed)node.eps)inList.iter(func->visitc.nodec.tagsc.delayed)seeds;List.rev!acc(* Splits the moves into pairwise-disjoint character sets. Moves come in
priority order and a configuration is appended last to each piece it
overlaps, so seeds stay in priority order. *)letsplit_moves(moves:(Cset.t*addrconfig)list):(Cset.t*addrconfiglist)list=letadd_movepieces(cset,cfg)=letrecinsertcsetpieces=ifCset.is_emptycsetthenpieceselse(matchpieceswith|[]->[(cset,[cfg])]|(piece,seeds)::rest->letinter=Cset.intersectionpiececsetinifCset.is_emptyinterthen(piece,seeds)::insertcsetrestelse(letpiece_only=Cset.differencepieceinterinletcset_rest=Cset.differencecsetinterinletwith_cfg=(inter,seeds@[cfg])inifCset.is_emptypiece_onlythenwith_cfg::insertcset_restrestelse(piece_only,seeds)::with_cfg::insertcset_restrest))ininsertcsetpiecesinList.fold_leftadd_move[]moves(* Mutable state of the construction *)(* Cell allocation. Canonical cells [0 .. num_logical - 1] (cell = tag id)
are only written by final operations; working registers live above. *)moduleRegisters:sigtypetvalcreate:num_logical:int->t(* A fresh working register. *)valalloc:t->cell(* As opposed to a canonical cell. *)valis_working:t->cell->bool(* Canonical and working cells. *)valcount:t->intend=structtypet={num_logical:int;mutablenext_cell:int}letcreate~num_logical={num_logical;next_cell=num_logical}letalloct=letc=t.next_cellint.next_cell<-c+1;cletis_workingtc=c>=t.num_logicalletcountt=t.next_cellend(* State identity modulo register renaming: registers are numbered by first
occurrence, so equal keys mean same nodes, same order, same sharing. *)moduleState_key:sigtypetvalof_candidate:candidate->tmoduleTbl:Hashtbl.Swithtypekey=tend=structtypet=(int*(int*int)list*(int*write)list)listletof_candidateconfigs=lettbl=Hashtbl.create8inletcanontaga=matchHashtbl.find_opttbl(tag,a)with|Somei->i|None->leti=Hashtbl.lengthtblinHashtbl.addtbl(tag,a)i;iinList.map(func->(c.node.id,List.map(fun(t,a)->(t,canonta))(TagMap.bindingsc.tags),TagMap.bindingsc.delayed))configsmoduleTbl=Hashtbl.Make(structtypenonrect=tletequal=(=)lethash=Hashtbl.hashend)end(* DFA states discovered so far, numbered in creation order. *)typestate_table={by_key:intState_key.Tbl.t;configs:(int,stored)Hashtbl.t;defs:(int,dfa_state)Hashtbl.t;mutablen_states:int;}letmake_state_table():state_table={by_key=State_key.Tbl.create31;configs=Hashtbl.create31;defs=Hashtbl.create31;n_states=0;}(* Registers a new state and returns its number. *)letadd_state(tbl:state_table)(key:State_key.t)(configs:stored):int=letnum=tbl.n_statesintbl.n_states<-num+1;State_key.Tbl.addtbl.by_keykeynum;Hashtbl.addtbl.configsnumconfigs;numtypectx={regs:Registers.t;rules:rulearray;tbl:state_table}(* New state: each [Pending] write gets a fresh cell. Returns the stored
state and the Set operations of the transition reaching it. *)letassign_cells(regs:Registers.t)(candidate:candidate):stored*tag_oplist=letassigned=Hashtbl.create4inletops=ref[]inletcell_for_newtagw=matchHashtbl.find_optassigned(tag,w)with|Somec->c|None->letc=Registers.allocregsinHashtbl.addassigned(tag,w)c;ops:=op_of_writecw::!ops;cinletconfigs=List.map(func->{cwithtags=TagMap.mapi(funtaga->matchawith|Cellcell->cell|Pendingw->cell_for_newtagw)c.tags;})candidatein(configs,!ops)(* Moves realigning [candidate]'s registers with those of the equal-keyed
[existing] state. A parallel move. *)letregister_moves(candidate:candidate)(existing:stored):tag_oplist=letmoves=refCellMap.emptyinletset_movedstop=matchCellMap.find_optdst!moveswith|None->moves:=CellMap.adddstop!moves|Someop'->assert(op=op')inList.iter2(funcandex->TagMap.iter(funtaga->letdst=TagMap.findtagex.tagsinmatchawith|Cellsrc->ifsrc<>dstthenset_movedst(Copy{dst;src})|Pendingw->set_movedst(op_of_writedstw))cand.tags)candidateexisting;List.mapsnd(CellMap.bindings!moves)(* Materializes the accepting configuration into the canonical cells: copies
of its registers, plus its delayed writes. *)letfinal_ops_of(regs:Registers.t)(accepting:cellconfig):tag_oplist=letcopies=TagMap.fold(funtagcellacc->ifcell=tag||TagMap.memtagaccepting.delayedthenaccelse(assert(Registers.is_workingregscell);Copy{dst=tag;src=cell}::acc))accepting.tags[]inTagMap.fold(funtagwacc->op_of_writetagw::acc)accepting.delayedcopies(* The accepting rule is the lowest-numbered one whose final node is in the
state (first-match semantics), with its final operations. *)letaccept_of(ctx:ctx)(configs:stored):acceptoption=letn=Array.lengthctx.rulesinletrecfirst_acceptingrule=ifrule=nthenNoneelse(letfinal=ctx.rules.(rule).finalinmatchList.find_opt(func->c.node==final)configswith|Someaccepting->Some{rule;final_ops=final_ops_ofctx.regsaccepting}|None->first_accepting(rule+1))infirst_accepting0(* The number of [candidate]'s state, built depth first if new, and the
operations of the transition reaching it. *)letrecfind_or_add_state(ctx:ctx)(candidate:candidate):int*tag_oplist=letkey=State_key.of_candidatecandidateinmatchState_key.Tbl.find_optctx.tbl.by_keykeywith|Somenum->(num,register_movescandidate(Hashtbl.findctx.tbl.configsnum))|None->letconfigs,ops=assign_cellsctx.regscandidateinletnum=add_statectx.tblkeyconfigsinbuild_statectxnumconfigs;(num,ops)(* Computes and stores the definition of state [num]. *)andbuild_state(ctx:ctx)(num:int)(configs:stored):unit=lettrans=transitionsctxconfigsinletaccept=accept_ofctxconfigsinHashtbl.addctx.tbl.defsnum{trans;accept}(* Outgoing transitions: split the character moves into disjoint sets,
close each piece, and look the pieces up in character-set order, so that
state numbers follow that order.
End of input is apart. It does not advance the position, but it counts
as one more symbol read: a match that reads it is longer than the state's
own accept, so any configuration may read it. End of input is read once,
so what they reach is cut down to its accepting configuration, which has
no transitions, or dropped if it has none: reading end of input then
fails like any missing transition. *)andtransitions(ctx:ctx)(configs:stored):(Cset.t*int*tag_oplist)array=letis_finalc=Array.exists(funr->r.final==c.node)ctx.rulesinletmoves_of~eofc=(* Taking a transition performs the delayed writes. *)lettags=TagMap.fold(funtagwtags->TagMap.addtag(Pendingw)tags)c.delayed(TagMap.map(funcell->Cellcell)c.tags)inList.filter_map(fun(cset,node)->letcset=ifeofthenCset.intersectioncsetCset.eofelseCset.differencecsetCset.eofinifCset.is_emptycsetthenNoneelseSome(cset,{node;tags;delayed=TagMap.empty}))c.node.transinleton_eof=letseeds=List.concat(List.map(moves_of~eof:true)configs)inmatchList.find_optis_final(eps_closure(List.mapsndseeds))with|Someaccepting->[(Cset.eof,[accepting])]|None->[]inleton_chars=List.map(fun(cset,seeds)->(cset,eps_closureseeds))(split_moves(List.concat(List.map(moves_of~eof:false)configs)))inletpieces=Array.of_list(on_eof@on_chars)inArray.sort(fun(c1,_)(c2,_)->comparec1c2)pieces;Array.map(fun(cset,candidate)->letnum,ops=find_or_add_statectxcandidatein(cset,num,ops))pieces(* See the implementation overview at the top of this file. *)letcompile(rs:regexparray):compiled=letrules=Array.mapcompile_rersinletctx={regs=Registers.create~num_logical:!cur_tag;rules;tbl=make_state_table();}inletseeds=List.map(funr->{node=r.entry;tags=TagMap.empty;delayed=TagMap.empty})(Array.to_listrules)inletnum0,ops=find_or_add_statectx(eps_closureseeds)inassert(num0=0);(* The writes of the initial closure are delayed like any other. *)assert(ops=[]);{dfa=Array.initctx.tbl.n_states(Hashtbl.findctx.tbl.defs);num_tags=Registers.countctx.regs;}(* High-level compilation from IR.
[compile_ir] lowers [Ir.t] patterns into low-level regexps with tag
annotations, then compiles them via [compile]. The lowering phase decides
how to allocate tags for [as] bindings:
- [Start_plus n]: the position is [n] code points from the token start.
- [End_minus n]: the position is [n] code points before the token end.
- [Tag {tag; offset}]: read memory cell [tag] and add [offset].
When both boundaries of a capture can be expressed as [Start_plus] or
[End_minus], no memory cells are needed at all.
Or-patterns [(p1 as x) | (p2 as x)] additionally use discriminator cells:
integer values that record which branch was taken, so the code generator
can emit the correct position extraction at match time. *)typepos_expr=|Tagof{tag:int;offset:int}|Start_plusofint|End_minusofinttypecompiled_binding={name:string;start_pos:pos_expr;end_pos:pos_expr;disc:(int*int)list;}typecompiled_ir={dfa:dfa;num_tags:int;bindings:compiled_bindinglistarray;}(* [shift_pos pe delta] shifts a position expression by [delta] code points
(positive = forward, negative = backward). Returns [None] if either
argument is unknown. *)letshift_pospedelta=match(pe,delta)with|Some(Start_plusn),Somed->Some(Start_plus(n+d))|Some(End_minusn),Somed->Some(End_minus(n-d))|Some(Tag{tag;offset}),Somed->Some(Tag{tag;offset=offset+d})|_->Noneletadvancepelen=shift_pospelenletretreatpelen=shift_pospe(Option.mapInt.neglen)(* [add_discriminators branches] takes a list of [(regexp, bindings)] pairs
from an n-ary alternation and wraps each branch with a discriminator tag
so the generated code can tell which branch matched. Branches with
identical bindings share the same discriminator value. If all branches
have identical bindings, no discriminator cell is allocated. *)letadd_discriminators(branches:(regexp*compiled_bindinglist)list)=letfold_alt=function|[]->assertfalse|(r,_)::rest->List.fold_left(funacc(r,_)->altaccr)rrestin(* Check if all branches produce identical bindings — if so, no
discriminator is needed at all. *)letall_same=matchbrancheswith|[]|[_]->true|(_,first)::rest->List.for_all(fun(_,tags)->tags=first)restinifall_samethen(fold_altbranches,snd(List.hdbranches))else(letdisc_cell=new_disc_cell()inletstampvaluetags=List.map(fun(ti:compiled_binding)->{tiwithdisc=(disc_cell,value)::ti.disc})tagsin(* Assign discriminator values. Branches with identical bindings
share the same value. *)letnext_val=ref0inletseen:(compiled_bindinglist*int)listref=ref[]inletget_valuetags=matchList.assoc_opttags!seenwith|Somev->v|None->letv=!next_valinincrnext_val;seen:=(tags,v)::!seen;vinletwrapped=List.map(fun(r,tags)->letv=get_valuetagsin(bind_discrdisc_cellv,stampvtags))branchesin(fold_altwrapped,List.concat_mapsndwrapped))(* [lower ir ~left ~right] converts an IR pattern to a low-level regexp
and a list of compiled bindings. [left] and [right] are the known
position contexts at the start and end of this pattern element. *)letreclower~left~right(ir:Ir.t):regexp*compiled_bindinglist=matchirwith|Ir.Charscset->(charscset,[])|Ir.Eps->(eps,[])|Ir.Starinner->letr,_=lower~left:None~right:Noneinnerin(repr,[])|Ir.Plusinner->letr,_=lower~left:None~right:Noneinnerin(plusr,[])|Ir.Rep(inner,n,m)->letr,_=lower~left:None~right:Noneinnerin(repeatrnm,[])|Ir.Capture(name,inner)->(* Named capture — try to derive each boundary from [left]/[right]
context or [fixed_length]; allocate tags only for boundaries that
cannot be computed statically. Best case: 0 tags. Worst case: 2. *)letr,tags=lower~left~rightinnerinletelem_len=Ir.fixed_lengthinnerinletknown_start=matchleftwithSome_->left|None->retreatrightelem_leninletknown_end=matchrightwith|Some_->right|None->advanceknown_startelem_leninletst,et,r=match(known_start,known_end)with|Somest,Someet->(st,et,r)|Somest,None->letwrapped,end_tag=bind_end_onlyrin(st,Tag{tag=end_tag;offset=0},wrapped)|None,Someet->letwrapped,start_tag=bind_start_onlyrin(Tag{tag=start_tag;offset=0},et,wrapped)|None,None->(matchelem_lenwith|Somelen->letwrapped,start_tag=bind_start_onlyrin(Tag{tag=start_tag;offset=0},Tag{tag=start_tag;offset=len},wrapped)|None->letwrapped,start_tag,end_tag=bindrin(Tag{tag=start_tag;offset=0},Tag{tag=end_tag;offset=0},wrapped))in(r,{name;start_pos=st;end_pos=et;disc=[]}::tags)|Ir.Altbranches->letlowered=List.map(lower~left~right)branchesinadd_discriminatorslowered|Ir.Seqelems->(* Sequence — propagate left/right position contexts through elements.
Right positions are computed right-to-left; left positions are
updated left-to-right after lowering each element. *)let_,rights=List.fold_right(fune(acc,l)->(retreatacc(Ir.fixed_lengthe),acc::l))elems(right,[])in(* Fallback for the left context: if [advance] returns [None]
(because the current left is unknown or the element has
variable length), but the element was a [Capture] whose
end position is a [Tag], we can use that tag as the [left]
anchor for the next element — it records a runtime position.
[Start_plus]/[End_minus] endpoints don't help here: they
are already factored into [advance], so if [advance] failed,
they have nothing more to offer. *)letleft_from_end_tagirtags'=matchirwith|Ir.Capture_->(matchtags'with|{end_pos=Tag_aset;_}::_->Someet|_->None)|_->Nonein(* [seq] is function composition and [eps] its identity, so the
fold needs no special case for the first element. *)let_,r_acc,tags_acc=List.fold_left2(fun(cur_left,r_acc,tags_acc)eright->letr',tags'=lower~left:cur_left~righteinletnew_left=matchadvancecur_left(Ir.fixed_lengthe)with|Some_ass->s|None->left_from_end_tagetags'in(new_left,seqr_accr',tags_acc@tags'))(left,eps,[])elemsrightsin(r_acc,tags_acc)letcompile_ir(rules:Ir.tarray)=Array.iter(funir->Ir.check_invariantir)rules;reset_tags();letlowered=Array.map(funir->lower~left:(Some(Start_plus0))~right:(Some(End_minus0))ir)rulesinletregexps=Array.mapfstloweredinletbindings=Array.mapsndloweredinletcompiled=compileregexpsin{dfa=compiled.dfa;num_tags=compiled.num_tags;bindings}letcset_to_labelcset=letescape_dotc=matchcwith|'"'->"\\\""|'\\'->"\\\\"|'<'->"\\<"|'>'->"\\>"|_->String.make1cinletformat_interval(lo,hi)=iflo=-1&&hi=-1then"EOF"elseiflo=hitheniflo>=32&&lo<=126then"'"^escape_dot(Char.chrlo)^"'"elsePrintf.sprintf"U+%04X"loelseiflo>=32&&lo<=126&&hi>=32&&hi<=126then"'"^escape_dot(Char.chrlo)^"'-'"^escape_dot(Char.chrhi)^"'"elsePrintf.sprintf"U+%04X-U+%04X"lohiinString.concat", "(List.mapformat_interval(cset:Cset.t:>(int*int)list))lettag_op_to_string=function|Set_position{dst}->"t"^string_of_intdst|Set_value{dst;value}->"d"^string_of_intdst^"="^string_of_intvalue|Copy{dst;src}->"t"^string_of_intdst^"<-t"^string_of_intsrcletdfa_to_dotdfa=letbuf=Buffer.create1024inletbprintf=Printf.bprintfinbprintfbuf"digraph {\n";bprintfbuf" rankdir=LR;\n";bprintfbuf" node [shape=circle];\n\n";bprintfbuf" _start [shape=point];\n";bprintfbuf" _start -> state0;\n\n";Array.iteri(funi{trans;accept}->(matchacceptwith|None->bprintfbuf" state%d [label=\"%d\"];\n"ii|Some{rule;final_ops}->letops=iffinal_ops=[]then""else"\\n{"^String.concat","(List.maptag_op_to_stringfinal_ops)^"}"inbprintfbuf" state%d [label=\"%d\\n[rule %d]%s\", shape=doublecircle];\n"iiruleops);Array.iter(fun(cset,target,tags)->letlabel=cset_to_labelcsetinletlabel=iftags=[]thenlabelelselabel^" {"^String.concat","(List.maptag_op_to_stringtags)^"}"inbprintfbuf" state%d -> state%d [label=\"%s\"];\n"itargetlabel)trans)dfa;bprintfbuf"}\n";Buffer.contentsbuf