123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886exceptionSyntax_errorof(Lexing.position*Lexing.position)*Wax_utils.Message.ttypesyntax_error={location:Wax_utils.Ast.location;message:Wax_utils.Message.t;related:Wax_utils.Diagnostic.labellist;}typesync_class=Open|Close|Boundary|Leader|Terminal|Skip(* Internal marker raised by [fail_detailed] to carry a structured error out of
[loop_handle] to [parse_diagnostics]; never escapes this module. *)exceptionDetailed_errorofsyntax_error(* Helpers independent of the functor parameters, shared by both {!Make} and
{!Make_parser}. *)moduleE=MenhirLib.ErrorReportsmoduleLu=MenhirLib.LexerUtilletsucceedv=vletshowtextpositions=E.extracttextpositions|>E.sanitize|>E.compress|>E.shorten20(* max width 43 *)letreport_syntax_error?(related=[])~colorsource(loc_start,loc_end)msg=lettheme=Wax_utils.Diagnostic.get_theme?color()inWax_utils.Diagnostic.output_error_with_source~theme~source~severity:Error~location:{loc_start;loc_end}~relatedmsg;(* The diagnostic has been printed; re-raise so the caller decides how to
terminate rather than exiting the process here. The CLI maps this to exit
code 128 (rejected input, like a validation or type error, not the
usage-error code; see the exit-code contract in bin/main.ml), while an
in-process embedder can catch it instead of having the whole process die. *)raise(Syntax_error((loc_start,loc_end),msg))letreadfilename=In_channel.with_open_binfilenameIn_channel.input_allletinitialize_lexingfilenametext=letlexbuf=Sedlexing.Utf8.from_stringtextinSedlexing.set_filenamelexbuffilename;lexbuf(* [Lexer.token] returns the tokenizer and a [start_override] ref: for a
compound opener ([(param], [(then], …) the lexer reads the [(] and its
keyword as two lexemes, so the lexbuf's reported start is the keyword's; the
ref carries the [(]'s position instead, so the token's span really begins at
its opening parenthesis. *)letlexer_lexbuf_to_supplier(lexer,start_override)(lexbuf:Sedlexing.lexbuf)()=lettoken=lexerlexbufinletstartp,endp=Sedlexing.lexing_bytes_positionslexbufinletstartp=match!start_overridewithSomep->p|None->startpin(token,startp,endp)(* Core parser over a Menhir incremental API, without the fast parser: the
incremental parser produces both the AST (happy path) and the error (with
[Parser_messages]) in a single pass. Provides [parse_diagnostics] (structured
error, no printing) and [parse_from_string] (prints and re-raises). *)moduleMake(Output:sigtypetend)(Tokens:sigtypetokenend)(Parser:sigmoduleMake(_:sigtypet=Wax_utils.Trivia.contextvalcontext:tend):sigtypetoken=Tokens.tokenmoduleMenhirInterpreter:MenhirLib.IncrementalEngine.INCREMENTAL_ENGINEwithtypetoken=tokenmoduleIncremental:sigvalparse:Lexing.position->Output.tMenhirInterpreter.checkpointendendend)(Parser_messages:sigvalmessage:int->stringend)(Lexer:sigvaltoken:Wax_utils.Trivia.context->(Sedlexing.lexbuf->Tokens.token)*Lexing.positionoptionrefend)=structmoduleInner(Context:sigtypet=Wax_utils.Trivia.contextvalcontext:tend)=structmoduleP=Parser.Make(Context)letstatecheckpoint:int=matchcheckpointwith|P.MenhirInterpreter.HandlingErrorenv->(matchP.MenhirInterpreter.topenvwith|Some(Element(s,_,_,_))->P.MenhirInterpreter.numbers|None->0)|_->assertfalseletrecpositions_in_stackenvi=matchP.MenhirInterpreter.getienvwith|Some(Element(_,_,pos1,pos2))->iffalsethenFormat.eprintf"%d--%d@."pos1.pos_cnumpos2.pos_cnum;positions_in_stackenv(i+1)|None->()letgettextcheckpointi=matchcheckpointwith|P.MenhirInterpreter.HandlingErrorenv->(matchP.MenhirInterpreter.getienvwith|Some(Element(_,_,pos1,pos2))->showtext(pos1,pos2)|None->"???")|_->assertfalse(* Compute the structured diagnostic (location, message, related labels) for
a menhir syntax error, without printing anything. Both the printing
handler [fail] (for the CLI) and the non-printing [fail_detailed] (for
in-process/editor use via [parse_diagnostics]) build on this. *)letbuild_syntax_errortextbuffercheckpoint=letenv=matchcheckpointwith|P.MenhirInterpreter.HandlingErrorenv->env|_->assertfalseinpositions_in_stackenv0;letlocation=E.lastbufferinlets=statecheckpointinletmessage=tryParser_messages.messageswithNot_found->Printf.sprintf"Syntax error (%d)\n"sinletmessage=ifmessage="<YOUR SYNTAX ERROR MESSAGE HERE>\n"thenPrintf.sprintf"Syntax error (%d)\n"selsemessageinletmessage=E.expand(gettextcheckpoint)messageinletlines=String.split_on_char'\n'messageinletrelated=ref[]inletmain_message=ref[]inList.iter(funline->letlen=String.lengthlineiniflen>2&&line.[0]='<'thentryleti=String.indexline'>'inletdepth=int_of_string(String.subline1(i-1))inletmsg=String.trim(String.subline(i+1)(len-i-1))inmatchP.MenhirInterpreter.get(depth-1)envwith|Some(Element(_,_,pos1,_pos2))->(* This hint points at a single opening delimiter, so underline
one character. The delimiter is normally the token's start —
the lexer gives a compound opener ([(then]/[(param]/…) the
'(' as its start — but a spurious reduction can surface a
plain token (e.g. ELEM) sitting just past the '('; in that
case walk the source back over blanks to the delimiter. *)letcnum=pos1.Lexing.pos_cnuminletis_delimc=c='('||c='['||c='{'inletblankc=c=' '||c='\t'inletdcnum=ifcnum<String.lengthtext&&is_delimtext.[cnum]thencnumelseletrecbacki=ifi<0||not(blanktext.[i])thenifi>=0&&is_delimtext.[i]thenielsecnumelseback(i-1)inback(cnum-1)inletstart={pos1withLexing.pos_cnum=dcnum}inletloc={Wax_utils.Ast.loc_start=start;loc_end={startwithLexing.pos_cnum=dcnum+1};}inrelated:={Wax_utils.Diagnostic.location=loc;message=Wax_utils.Message.textmsg;}::!related|None->main_message:=line::!main_messagewith_->main_message:=line::!main_messageelsemain_message:=line::!main_message)lines;letmain_message=List.rev!main_messageinletrelated_labels=List.rev!relatedin(* Remove trailing empty line if it was caused by a trailing newline and we have related labels *)letmain_message=matchList.revmain_messagewith|""::restwhenrelated_labels<>[]->List.revrest|_->main_messageinletmessage=String.concat"\n"main_messagein(location,message,related_labels)letfail_detailedtextbuffercheckpoint=let(loc_start,loc_end),message,related=build_syntax_errortextbuffercheckpointinraise(Detailed_error{location={Wax_utils.Ast.loc_start;loc_end};message=Wax_utils.Message.textmessage;related;})(* Parse, returning the AST or a structured syntax error, without printing:
the incremental parser produces both, in a single pass. *)letparse_diagnostics~filenametext=letlexbuf=initialize_lexingfilenametextinletsupplier=lexer_lexbuf_to_supplier(Lexer.tokenContext.context)lexbufinletbuffer,supplier=E.wrap_suppliersupplierinletcheckpoint=P.Incremental.parse(snd(Sedlexing.lexing_bytes_positionslexbuf))intryOk(P.MenhirInterpreter.loop_handlesucceed(fail_detailedtextbuffer)suppliercheckpoint)with|Detailed_errore->Errore|Syntax_error((loc_start,loc_end),msg)->Error{location={Wax_utils.Ast.loc_start;loc_end};message=msg;related=[];}|Sedlexing.InvalidCodepoint_|Sedlexing.MalFormed->letloc_start,loc_end=Sedlexing.lexing_bytes_positionslexbufinError{location={Wax_utils.Ast.loc_start;loc_end};message=Wax_utils.Message.text"Input file contains malformed UTF-8 byte sequences";related=[];}(* Panic-mode error recovery, sync-token variant. This uses only the
vanilla [INCREMENTAL_ENGINE] API — no [error] productions in the grammar
and no inspection-API [feed] — so it needs neither a grammar change nor
[--inspection] on the generated parser. We drive [offer]/[resume] by hand
(rather than [loop_handle], which stops at the first error) so that at a
[HandlingError] checkpoint we still hold the offending token: to recover
we skip forward to the next boundary token, unwind the stack with [pop]
to a state that [acceptable] confirms can shift it, [offer] it, and carry
on. Every error is collected; the returned AST is whatever the parser
reduces to, with holes where erroneous spans were skipped. *)letparse_recover~filename~sync?(insert=[])?(closers=[])?barriertext=letmoduleMI=P.MenhirInterpreterinletlexbuf=initialize_lexingfilenametextinleterrors=ref[]in(* Byte offset of the last position at which a token was inserted (see
[try_insert]); guards against inserting twice at the same spot, which
would otherwise loop when the insertion does not actually unblock the
parse. *)letlast_insert=ref(-1)in(* Raised when the lexer cannot make progress past a malformed byte; the
error is already recorded, so the top-level handler just stops. *)letexceptionLexing_gave_upinletrecord_errorlocationmessage=errors:={location;message;related=[]}::!errorsin(* Lexer-level recovery. A bad character or malformed byte makes
[Lexer.token] raise rather than surface as a parser [HandlingError];
the lexer has already consumed the offending lexeme (e.g. the [Compl]
catch-all matches one code point, then raises), so we record the error
and retry, resuming past it — a stray character no longer truncates the
whole parse. The position guard keeps the [parse_recover] termination
invariant: if a raise made no progress (a byte the decoder cannot even
skip), we give up lexing rather than spin. *)letbase_supplier=lexer_lexbuf_to_supplier(Lexer.tokenContext.context)lexbufinletcnum()=(Sedlexing.lexing_bytes_position_currlexbuf).Lexing.pos_cnuminletrecrecovering_supplier()=letbefore=cnum()in(* Error just recorded; retry past the offending lexeme, or give up if
the raise made no progress. *)letresume()=ifcnum()>beforethenrecovering_supplier()elseraiseLexing_gave_upintrybase_supplier()with|Syntax_error((loc_start,loc_end),message)->record_error{Wax_utils.Ast.loc_start;loc_end}message;resume()|Sedlexing.InvalidCodepoint_|Sedlexing.MalFormed->letloc_start,loc_end=Sedlexing.lexing_bytes_positionslexbufinrecord_error{Wax_utils.Ast.loc_start;loc_end}(Wax_utils.Message.text"Input file contains malformed UTF-8 byte sequences");resume()inletbuffer,supplier=E.wrap_supplierrecovering_supplierin(* Net open-parenthesis depth of the input consumed so far. Every token is
pulled from [supplier] exactly once — by [run] on the happy path, by
[find_sync] while skipping — so counting openers/closers here tracks the
{e source} nesting at the current read position: how many openers
enclose it, independent of parser state. Group-drop consults it to tell a
genuinely-open inner group from a stray closer that follows an
already-complete construct (where nothing is open). *)letparen_depth=ref0inletsupplier()=let((t,_,_)astok)=supplier()in(matchsynctwith|Open->incrparen_depth|Close->decrparen_depth|Boundary|Leader|Terminal|Skip->());tokinletrecordcheckpoint=let(loc_start,loc_end),message,related=build_syntax_errortextbuffercheckpointinerrors:={location={Wax_utils.Ast.loc_start;loc_end};message=Wax_utils.Message.textmessage;related;}::!errorsin(* A missing token, reported as a zero-width caret just before the
offending token (where the inserted token belongs) — unless an error was
already flagged ending at that very spot (only whitespace between). That
happens when the lexer skipped a bad token there (a bare ["$"] the user
is still turning into an identifier, say): its error already reports the
gap, and the placeholder is inserted only to keep the tree well-formed,
so a second "Missing …" caret on top would be redundant. *)letonly_blank_betweenlohi=letok=ref(lo<=hi)infori=lotohi-1domatchtext.[i]with|' '|'\t'|'\n'|'\r'->()|_->ok:=falsedone;!okinletrecord_missingmessage(pos:Lexing.position)=letalready_flagged=match!errorswith|{location;_}::_->only_blank_betweenlocation.Wax_utils.Ast.loc_end.Lexing.pos_cnumpos.pos_cnum|[]->falseinifnotalready_flaggedthenerrors:={location={Wax_utils.Ast.loc_start=pos;loc_end=pos};message;related=[];}::!errorsin(* Return the token to resynchronize on: the next boundary (or terminal)
reached by discarding tokens from the supplier. [tok0] is the token
already in hand (the one that triggered the error, if any); if it is
itself a boundary we resynchronize on it directly rather than skipping
past it.
[depth] tracks bracket nesting {e entered while skipping}, so a boundary
that belongs to a construct opened inside the skipped span does not
resynchronize the enclosing one: an [Open] descends a level, a [Close]
at depth 0 is a genuine enclosing boundary but otherwise just ascends a
level, and a [Boundary] (e.g. [";"]) counts only at depth 0. A [Leader]
(an item/statement-leading keyword) resynchronizes at any depth — an
unbalanced opener must never swallow the next top-level item, which is
the whole reason those keywords are boundaries. Always terminates: every
step that does not stop pulls one token, advancing toward the
end-of-input [Terminal]. *)(* Returns [`Sync tok] (resynchronize on a single token) or, in a
parenthesized grammar with a [barrier], [`Barrier (toks, pos)] — the
tokens re-offered to start a new field — when the scan meets the start of
one at the {e enclosing} level. Two shapes: a bare [(] immediately
followed by a field keyword (offered as the pair [( ; kw]), or a fused
[(type]/[(import]/[(export] opener the lexer folds into one token
(offered alone). Both are honoured only at [depth = 0] — the level of the
construct being recovered — so a missing closer cannot let depth-counting
swallow the sibling, while a field-like opener nested in content being
skipped (a [(func] inside a [(type … (func))] functype) stays ordinary
content and is descended into, not mistaken for a new field. *)letrecfind_syncdepthtok0=letstep((t,tsp,_)astok)=matchsynctwith|Skip->find_syncdepthNone|Open->(matchbarrierwith|None->find_sync(depth+1)None|Some(_,is_leader,is_fused)->ifis_fusedtthenifdepth=0then`Barrier([t],tsp)elsefind_sync(depth+1)Noneelselet((t2,sp2,_)astok2)=supplier()inifdepth=0&&is_leadert2then`Barrier([t;t2],sp2)elsefind_sync(depth+1)(Sometok2))|Close->ifdepth>0thenfind_sync(depth-1)Noneelse`Synctok|Boundary->ifdepth>0thenfind_syncdepthNoneelse`Synctok|Leader|Terminal->`Synctokinmatchtok0withSometok->steptok|None->step(supplier())in(* Pop the parser stack to the closest state that could shift [tok] and
return that env, {e without} offering [tok]. Group-drop uses it to climb
out of a broken inner group — the state reached is the enclosing context,
past the group's opener, from which the {e next} boundary (not this
closer) is resynchronized. [None] if no stacked state accepts [tok]. *)letrecpop_toenvtok(startp:Lexing.position)=ifMI.acceptable(MI.input_neededenv)tokstartpthenSomeenvelsematchMI.popenvwith|Someenv'->pop_toenv'tokstartp|None->Nonein(* Unwind the parser stack to the closest state that can shift [tok] and
shift it there ([pop_to] then offer), returning the resumed checkpoint;
[None] if no stacked state accepts it. *)letunwindenv((tok,startp,_endp)assync_tok)=matchpop_toenvtokstartpwith|Someenv'->Some(MI.offer(MI.input_neededenv')sync_tok)|None->Nonein(* Drive a checkpoint through shifts and reductions to the next decision
point ([InputNeeded]/[Accepted]/[HandlingError]/[Rejected]). *)letrecsettlecheckpoint=matchcheckpointwith|MI.Shifting_|MI.AboutToReduce_->settle(MI.resumecheckpoint)|_->checkpointin(* Try to recover by inserting a missing token (typically a statement
separator [";"]) in front of the offending token, rather than skipping
to a boundary. When the erroring state can shift [insert] — [acceptable]
answers this directly, no need to read the error message — offer a
zero-width [insert] there. But [acceptable] only proves the {e inserted}
token fits, not that the {e held} (offending) token then does: inserting
[";"] before an [@] that cannot start a statement would just add a
spurious "Missing ';'" on top of the real error. So we validate the
repair by offering the held token on top and requiring that it too be
consumed — the parser must reach the next [InputNeeded] (held shifted,
wanting more input) or [Accepted], not an error state. Only a validated
repair is recorded and returned, with the held token already consumed;
otherwise [None] falls through to skip-based recovery. Attempted at most
once per source position ([last_insert]) so it cannot loop. *)lettry_insertenvlast=matchlastwith|Some((_,startp,_)asheld)wheninsert<>[]&&!last_insert<>startp.Lexing.pos_cnum->last_insert:=startp.Lexing.pos_cnum;letcp=MI.input_neededenvin(* Try each candidate token in order; keep the first that both is
acceptable and, once the held token is offered on top, leaves the
parser wanting more input or accepting (the validation check). *)letattempt(tok,message)=ifnot(MI.acceptablecptokstartp)thenNoneelsematchsettle(MI.offercp(tok,startp,startp))with|MI.InputNeeded_asafter_insert->(matchsettle(MI.offerafter_insertheld)with|(MI.InputNeeded_|MI.Accepted_)asafter_held->record_missingmessagestartp;Someafter_held|_->None)|_->NoneinList.find_mapattemptinsert|_->Nonein(* When the offending token is itself a structural boundary — a closing
bracket, the statement separator, or EOF — but is rejected because an
inner construct in front of it is still open, the generic [skip] would
unwind {e past} that inner construct to a state that accepts the
boundary, dropping it. E.g. in ["fn f() { add(1, 2 }"] the unclosed
[add(1, 2] is discarded so [f]'s body is empty, and at EOF the whole
function the user is still typing is lost. So first try to {e auto-close}
the inner construct: repeatedly insert whichever [closers] token (or, in
between, the [insert] separator) the parser will accept until the
offending token itself becomes acceptable, then offer it — so the inner
construct reduces into the best-effort AST before the boundary consumes
it. Returns the resumed checkpoint, or [None] to fall through to [skip].
The syntax error itself is still recorded by the caller; only the
recovered AST improves.
A closer is always preferred; the separator only steps in to end a
statement that must be terminated before its block can close (e.g.
["add(1, 2 }"] needs ")" then ";" then "}"). Termination: every inserted
closer shifts a closing bracket, which strictly reduces the open-bracket
nesting, so only finitely many are inserted. The separator would
otherwise self-loop — a bare ";" is a valid {e empty} statement, so it
stays acceptable in a statement list forever — so it is allowed only when
the previous insertion was not itself a separator ([prev_sep]); no two
separators run consecutively, and a closer or the target must follow.
[fuel] is a final backstop, not the real bound. *)(* Insert acceptable [closers] one at a time — or, when none fits and
[with_insert] is set, an acceptable [insert] candidate (a statement
separator, or a placeholder operand that lets a construct complete), but
never two non-closers in a row — up to [fuel] steps, until [goal
checkpoint] returns [Some]. Shared by [close_pending] (auto-close a
construct in front of a boundary, [~with_insert:true]) and [place_field]
(barrier placement, [~with_insert:false] — closers only). Termination:
every inserted closer shifts a closing bracket, strictly reducing the
open-bracket nesting, and no two non-closers run consecutively (a bare
[";"] is a valid empty statement, so it would otherwise self-loop), so
only finitely many are inserted; [fuel] is a backstop. [pos] is the
zero-width position the inserted tokens carry. *)letinsert_to_goal~goal~with_insertposcheckpoint=letrecloopcheckpointprev_insertfuel=iffuel<=0thenNoneelsematchgoalcheckpointwith|Some_asr->r|None->(matchList.find_opt(func->MI.acceptablecheckpointcpos)closerswith|Somec->loop(settle(MI.offercheckpoint(c,pos,pos)))false(fuel-1)|None->(if(notwith_insert)||prev_insertthenNoneelsematchList.find_opt(fun(tok,_)->MI.acceptablecheckpointtokpos)insertwith|Some(tok,_)->loop(settle(MI.offercheckpoint(tok,pos,pos)))true(fuel-1)|None->None))inloopcheckpointfalse1000inletclose_pendingenvlast=match(last,closers)with|Some((t,pos,_)astarget),_::_whenmatchsynctwith|Close|Boundary|Terminal->true|Open|Leader|Skip->false->insert_to_goal~with_insert:truepos(MI.input_neededenv)~goal:(funcp->ifMI.acceptablecptposthenSome(settle(MI.offercptarget))elseNone)|_->Nonein(* A fully-parenthesized grammar (WAT) has no separator or leader token, so
a {e missing closer} — [(module (func (i32.const 1) (func …] — surfaces
as a field-opening keyword offered where an instruction was expected:
the [(] before it shifted as a folded-instruction opener, then the
keyword ([func]) errors. Depth-counting would then swallow the sibling.
[barrier] names that shape: a token [(] to re-offer and a predicate
recognizing the keyword. On such an error, pop the spurious [(] off the
stack ([MI.pop]) and, from the state before it, insert closers until
re-offering [(] leaves the keyword acceptable (i.e. we have climbed to
the field level, closing the enclosing construct on the way) — then
offer [(] and the keyword, so the enclosing field reduces into the AST
and the new one starts. [None] falls through to [skip]. The two-token
trial (offer [(], test the keyword) is why a bare "[(] is acceptable"
check is not enough: [(] is acceptable at every nesting level. *)(* Re-offer a barrier pair [( <keyword>] — the start of a new field in a
parenthesized grammar — from the closest level that accepts it, so the
enclosing (broken) field reduces into the AST and the new one starts. A
two-token trial is essential: [(] alone is acceptable at every nesting
level (it starts a folded instruction), so we must offer [(] then the
keyword and require the keyword to settle. We reach that level only by
inserting closers (a {e missing} closer — climb by closing the enclosing
field, keeping its body), which is value-preserving and fuel-bounded.
Climbing by [MI.pop] instead would discard the semantic values of any
construct already reduced onto the stack (a stray [)] that closed a
module early would lose all its fields), so it is not attempted; when
insertion cannot reach an accepting level, [None] falls through to
[skip]. *)letplace_fieldenvtokspos=(* Offer the barrier's tokens in sequence — [( ; <keyword>] for a bare
opener, or the single fused [(type]/[(import]/[(export] token — from
the closest level that accepts them, requiring the last to settle to
[InputNeeded]/[Accepted]. The multi-token trial is why a bare "[(] is
acceptable" check is not enough: [(] (and a fused opener, valid both as
a module field and nested) is acceptable at more than one level. *)letoffer_allcheckpoint=letrecgocp=function|[]->(matchcpwith|MI.InputNeeded_|MI.Accepted_->Somecp|_->None)|tok::rest->(matchcpwith|MI.InputNeeded_whenMI.acceptablecptokpos->go(settle(MI.offercp(tok,pos,pos)))rest|_->None)ingocheckpointtoksin(* Climb by inserting closers (value-preserving, no [insert] candidates)
until the barrier tokens settle; see [insert_to_goal]. *)insert_to_goal~goal:offer_all~with_insert:falsepos(MI.input_neededenv)in(* Direct-error barrier route: the held token is a field keyword whose [(]
already shifted as a folded-instruction opener (the [(module (func …
(func …] missing-closer shape). Pop that one cell, then place the pair
from the state before it. The other route — the barrier met while
scanning — is handled in [skip] via [find_sync]'s [`Barrier].
It must only fire on a keyword genuinely written [( keyword]: the token
offered just before it ([prev]) has to be the bare [(], else popping a
cell would fabricate a spurious field from a keyword typed bare as an
instruction ([(func (nop) memory)], where [prev] is [)]). [prev] is the
exact previous token, so a comment between the [(] and the keyword — which
a raw-source scan would trip over — is irrelevant. *)lettry_barrierenvprevlast=match(barrier,prev,last)with|Some(lparen,is_leader,_),Some(pt,_,_),Some(t,pos,_)whenis_leadert&&pt=lparen->(matchMI.popenvwith|Someenv'->place_fieldenv'[lparen;t]pos|None->None)|_->Nonein(* Main loop: [last] is the most recently offered token, so at a
[HandlingError] it is the token that provoked the error; [prev] is the
token offered just before it, which [try_barrier] consults to tell a
field keyword genuinely written [( keyword] from one typed bare as an
instruction. *)letrecruncheckpointprevlast=matchcheckpointwith|MI.InputNeeded_->lettok=supplier()inrun(MI.offercheckpointtok)last(Sometok)|MI.Shifting_|MI.AboutToReduce_->run(MI.resumecheckpoint)prevlast|MI.HandlingErrorenv->recovercheckpointenvprevlast|MI.Acceptedv->Somev|MI.Rejected->Noneandrecovercheckpointenvprevlast=matchtry_insertenvlastwith|Somecheckpoint->runcheckpointNoneNone|None->((* Insertion did not apply: record the standard error, then either
auto-close an inner construct still open in front of the boundary
or skip to a boundary. *)recordcheckpoint;matchclose_pendingenvlastwith|Somecheckpoint->runcheckpointNoneNone|None->(matchtry_barrierenvprevlastwith|Somecheckpoint->runcheckpointNoneNone|None->skipenvlast))andskipenvlast=matchfind_sync0lastwith|`Barrier(toks,pos)->place_barrierenvtokspos|`Sync((tok,startp,_)assync_tok)->(* Group-drop (parenthesized grammars only, hence the [barrier]
guard): the resync token is a closer [)] that the error state
cannot itself shift — it closes an {e inner} group whose
production is still incomplete, e.g. the missing multi-token
operand of [(v128.const)]. Offering it via [unwind] would climb to
an ancestor and consume {e that} ancestor's closer instead,
dropping the enclosing field ([(func (v128.const))] would lose the
whole [func]). So drop the broken group — pop past its opener and
discard its closer — and resynchronize on the next boundary, which
the enclosing construct can then close normally. A closer that the
error state {e can} shift closes the construct legitimately (junk
in an otherwise-complete body), so it is offered as before.
Progresses — a token is consumed — so recovery still terminates.
Guarded on the source nesting ([paren_depth] minus this closer's
own contribution, i.e. the openers still enclosing it): group-drop
is only meaningful when an inner group is genuinely open. A stray
[)] after an already-complete construct — [(module (func (nop))) )]
— sits at depth 0 with nothing open, and popping toward it would
climb into the finished construct and discard it; there it is left
to [offer_sync], which simply drops it. *)letenclosing_depth=!paren_depth-matchsynctokwithOpen->1|Close->-1|_->0inifbarrier<>None&&(matchsynctokwithClose->true|_->false)&&enclosing_depth>0&¬(MI.acceptable(MI.input_neededenv)tokstartp)thengroup_dropenvtokstartpsync_tokelseoffer_syncenvsync_tokandgroup_dropenvtokstartpsync_tok=(* Pop past the broken group's opener so the barrier or the next closer
lands in the enclosing context, not grafted onto the incomplete group
(e.g. [(import "m")] must not absorb the following [(func …)] as its
descriptor). If the stack cannot be climbed, fall back to offering the
closer where the error left us. *)matchpop_toenvtokstartpwith|None->offer_syncenvsync_tok|Someenv'->(matchfind_sync0Nonewith|`Barrier(toks,pos)->place_barrierenv'tokspos|`Syncsync_tok->offer_syncenv'sync_tok)andplace_barrierenvtokspos=(* A new field starts here: place the barrier tokens, closing the
enclosing construct as needed. A false barrier makes [place_field]
fail; drop it and scan on. *)matchplace_fieldenvtoksposwith|Somecheckpoint->runcheckpointNoneNone|None->skipenvNoneandoffer_syncenv((tok,_,_)assync_tok)=matchunwindenvsync_tokwith|Somecheckpoint->runcheckpointNoneNone|None->((* No stacked state can shift this boundary. At end of input there is
nothing left to try; otherwise drop this boundary and scan on for
the next one, keeping the same error state to unwind from. *)matchsynctokwith|Terminal->None|_->skipenvNone)in(* Lexer errors are handled by [recovering_supplier] above (recorded, then
skipped). [Lexing_gave_up] means it could not make progress, so stop —
the error is already recorded. The [Syntax_error]/[Sedlexing] arms are a
backstop for a raise from elsewhere (e.g. a grammar semantic action),
recorded here since the supplier did not see it. *)letstart=snd(Sedlexing.lexing_bytes_positionslexbuf)inletast=tryrun(P.Incremental.parsestart)NoneNonewith|Lexing_gave_up->None|Syntax_error((loc_start,loc_end),message)->record_error{Wax_utils.Ast.loc_start;loc_end}message;None|Sedlexing.InvalidCodepoint_|Sedlexing.MalFormed->letloc_start,loc_end=Sedlexing.lexing_bytes_positionslexbufinrecord_error{Wax_utils.Ast.loc_start;loc_end}(Wax_utils.Message.text"Input file contains malformed UTF-8 byte sequences");Nonein(ast,List.rev!errors)(* Printing variant, as the CLI expects: report the structured error (same
message and labels) and re-raise via [report_syntax_error]. *)letparse_from_string?color~filenametext=matchparse_diagnostics~filenametextwith|Okast->ast|Error{location={loc_start;loc_end};message;related}->report_syntax_error~related~colortext(loc_start,loc_end)messageendletparse_from_string?color~filenametext=Wax_utils.Debug.timed"parse"@@fun()->letctx=Wax_utils.Trivia.make()inletmoduleContext=structtypet=Wax_utils.Trivia.contextletcontext=ctxendinletmoduleI=Inner(Context)in(I.parse_from_string?color~filenametext,ctx)letparse?color~filename()=parse_from_string?color~filename(readfilename)letparse_diagnostics~filenametext=Wax_utils.Debug.timed"parse"@@fun()->letctx=Wax_utils.Trivia.make()inletmoduleContext=structtypet=Wax_utils.Trivia.contextletcontext=ctxendinletmoduleI=Inner(Context)inmatchI.parse_diagnostics~filenametextwith|Okast->Ok(ast,ctx)|Errore->Erroreletparse_recover~filename~sync?insert?closers?barriertext=Wax_utils.Debug.timed"parse"@@fun()->letctx=Wax_utils.Trivia.make()inletmoduleContext=structtypet=Wax_utils.Trivia.contextletcontext=ctxendinletmoduleI=Inner(Context)inletast,errors=I.parse_recover~filename~sync?insert?closers?barriertextin(ast,errors,ctx)end(* The full parser: {!Make} plus the fast parser, used for its speed on the
happy path. [parse_from_string] tries the fast parser and, on any failure,
falls back to the core's incremental [parse_from_string], which re-parses and
either succeeds or raises the reported syntax error. The fast attempt only
fails on a syntax error (both parsers accept the same grammar), so the
fallback always ends in that error; its partial trivia context is therefore
irrelevant and discarded. *)moduleMake_parser(Output:sigtypetend)(Tokens:sigtypetokenend)(Parser:sigmoduleMake(_:sigtypet=Wax_utils.Trivia.contextvalcontext:tend):sigtypetoken=Tokens.tokenmoduleMenhirInterpreter:MenhirLib.IncrementalEngine.INCREMENTAL_ENGINEwithtypetoken=tokenmoduleIncremental:sigvalparse:Lexing.position->Output.tMenhirInterpreter.checkpointendendend)(Fast_parser:sigmoduleMake(_:sigtypet=Wax_utils.Trivia.contextvalcontext:tend):sigtypetoken=Tokens.tokenexceptionErrorvalparse:(Lexing.lexbuf->token)->Lexing.lexbuf->Output.tendend)(Parser_messages:sigvalmessage:int->stringend)(Lexer:sigvaltoken:Wax_utils.Trivia.context->(Sedlexing.lexbuf->Tokens.token)*Lexing.positionoptionrefend)=structmoduleCore=Make(Output)(Tokens)(Parser)(Parser_messages)(Lexer)includeCoreletparse_from_string?color~filenametext=Wax_utils.Debug.timed"parse"@@fun()->letctx=Wax_utils.Trivia.make()inletmoduleContext=structtypet=Wax_utils.Trivia.contextletcontext=ctxendinletmoduleF=Fast_parser.Make(Context)inletlexbuf=initialize_lexingfilenametextintryletsupplier=lexer_lexbuf_to_supplier(Lexer.tokenContext.context)lexbufinletrevised_parser=MenhirLib.Convert.Simplified.traditional2revisedF.parsein(revised_parsersupplier,ctx)with|F.Error|Syntax_error_|Sedlexing.InvalidCodepoint_|Sedlexing.MalFormed->Core.parse_from_string?color~filenametextletparse?color~filename()=parse_from_string?color~filename(readfilename)end