123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718(** For error reporting it is nice to display the source filename and the source
code itself. They are optional. In fact, in situations like partial parsing
(REPL, auto-completion, etc.) we may not know the full source code of a
thunk. *)moduleState:sigtypet=private{origin:stringoption;(** The file or URL containing the source contents. *)source_contents:stringoption;(** The source contents, if available. This is used for error reporting
and syntax highlighting. *)downgrade_errors_into_warnings:bool;(** [true] if and only if all errors should be downgraded to warnings.
Use when parsing a file that will be skipped if it has problems. *)deny_deprecated_function_args:bool;(** [true] if and only if the function [args] field is allowed in values
files ["*.values.json"]. *)skip_first_n_terms:int;(** If > 0, skip the first [n] terms in the input. Useful for REPLs. *)}(** The state of the parser, which includes the source file and the starting
position. *)valcreate_without_source:?origin:string->?downgrade_errors_into_warnings:unit->?deny_deprecated_function_args:unit->?skip_first_n_terms:int->unit->tvalcreate_with_source:?origin:string->?downgrade_errors_into_warnings:unit->?deny_deprecated_function_args:unit->?skip_first_n_terms:int->string->t(** [create_with_source ?origin ?downgrade_errors_into_warnings
source_contents] creates an initial state at the starting position (line
1, column 1) of the source contents (ie. JSON if a thunk)
[source_contents].
The [origin] should be a human-readable identifier for the source
contents. A linkable identifier is even better for the end-user to click
in their favorite IDE. So if the source comes from a file, the [origin]
should be the file path relative to the project root or an absolute path.
If the source comes from the Internet/intranet, the [origin] should be the
URL. If the source comes from a REPL, the [origin] should be [None]. *)valnone:tvalorigin:t->stringoptionvalsource_contents:t->stringoptionvaldowngrade_errors_into_warnings:t->boolvaldeny_deprecated_function_args:t->boolvalskip_first_n_terms:t->intvalresolve_range_of_source:default:string->t->ThunkRanges.toption->string*ThunkRanges.t(** [resolve_range_of_source ~default state range] returns the source that
corresponds to the [range].
The use cases are:
- display the source code at [range] with its surrounding context if
possible; otherwise, display [default].
- parse the source code at [range] if possible so that error locations
correspond to the underlying source code; otherwise, parse [default] and
get error locations that correspond to [default].
The returned string for display, if the [range] is mapped, may be
different from [default] since it may include the original, unescaped
characters. That is by design since the original source code, if
available, is likely the most useful for the end-user.
When the [range] is [Some _] and the {!source_contents} are available and
[default] is the text in the corresponding range of {!source_contents}
(with one exception), then the returned string is the {!source_contents}
and the returned range is [range]. The [default] text is not compared to
the contents of {!source_contents} when the [range] is a mapped range
because the mapping may have changed the text by adding escape characters.
Otherwise, the returned string is [default] and the returned range is the
full range of [default]. *)end=structtypet={origin:stringoption;source_contents:stringoption;downgrade_errors_into_warnings:bool;deny_deprecated_function_args:bool;skip_first_n_terms:int;}letcreate_without_source?origin?downgrade_errors_into_warnings?deny_deprecated_function_args?(skip_first_n_terms=0)()={origin;source_contents=None;downgrade_errors_into_warnings=downgrade_errors_into_warnings=Some();deny_deprecated_function_args=deny_deprecated_function_args=None;skip_first_n_terms;}letcreate_with_source?origin?downgrade_errors_into_warnings?deny_deprecated_function_args?(skip_first_n_terms=0)source_contents={origin;source_contents=Somesource_contents;downgrade_errors_into_warnings=downgrade_errors_into_warnings=Some();deny_deprecated_function_args=deny_deprecated_function_args=None;skip_first_n_terms;}letnone={origin=None;source_contents=None;downgrade_errors_into_warnings=false;deny_deprecated_function_args=false;skip_first_n_terms=0;}letorigin{origin;_}=originletsource_contents{source_contents;_}=source_contentsletdowngrade_errors_into_warnings{downgrade_errors_into_warnings;_}=downgrade_errors_into_warningsletdeny_deprecated_function_args{deny_deprecated_function_args;_}=deny_deprecated_function_argsletskip_first_n_terms{skip_first_n_terms;_}=skip_first_n_termsletresolve_range_of_source~default{source_contents;_}rangeopt=match(source_contents,(rangeopt:ThunkRanges.toption))with|None,_|_,None->(* We have no source. *)(default,ThunkRanges.raw_range_of_stringdefault)|Someactual_source,Some(Raw_range(startpos,endpos)asrange)->(* Arrange the source so it corresponds to the range. *)letstart_idx_incl=Fmlib_parse.Position.byte_offsetstartposinletend_idx_excl=String.lengthdefault+start_idx_inclinifend_idx_excl<=String.lengthactual_source&&end_idx_excl=Fmlib_parse.Position.byte_offsetendpos&&String.equaldefault(String.subactual_sourcestart_idx_incl(end_idx_excl-start_idx_incl))then(actual_source,range)else(default,ThunkRanges.raw_range_of_stringdefault)|(Someactual_source,Some(Mapped_range{outer_range=_;inner_range=_,endpos}asrange))->(* Arrange the source so it corresponds to the mapped range.
There is no guarantee that the mapped range does not have
escape characters so we can't check to see if the raw characters
in the mapped range equal [default]. *)letend_idx_excl=Fmlib_parse.Position.byte_offsetendposinifend_idx_excl<=String.lengthactual_sourcethen(actual_source,range)else(default,ThunkRanges.raw_range_of_stringdefault)end(** Semantic errors that have locations and error text. *)moduleSemantic:sigtypet=private{error_range:Fmlib_parse.Position.range;error_message:string;error_brief:string;is_rendered:bool;}valcreate:Fmlib_parse.Position.range->string->tvalcreate_rendered:brief:string->Fmlib_parse.Position.range->string->tvalpp:Format.formatter->t->unitvalpp_message:Format.formatter->t->unitvalerror_range:t->Fmlib_parse.Position.rangevalerror_message:t->stringvalerror_brief:t->stringvalis_rendered:t->boolvalprepend_message:t->string->t(** [prepend_message t prefix] prepends [prefix] to the error message of [t].
You are responsible for adding newlines if needed. *)end=structtypet={error_range:Fmlib_parse.Position.range;error_message:string;error_brief:string;is_rendered:bool;(** [true] if the {!error_message} has been rendered with a
pretty-printer, [false] otherwise. *)}letfmt_wordsppfs=Format.(pp_print_list~pp_sep:pp_print_cutpp_print_string)ppf(String.split_on_char'\n's)letppppf{error_range;error_brief;error_message;is_rendered}=Format.fprintfppf"@[<v 2>Error: %a@;%a@,%a.@ Rendered: %b@]"fmt_wordserror_brieffmt_wordserror_message(ThunkRanges.pp_rangeNone)error_rangeis_renderedletpp_messageppf{error_message;_}=Format.fprintfppf"@[<v>%a@]"fmt_wordserror_messageletcreateerror_rangeerror_message={error_range;error_brief=error_message;error_message;is_rendered=false;}letcreate_rendered~brieferror_rangeerror_message={error_range;error_brief=brief;error_message;is_rendered=true}letprepend_messagetprefix={twitherror_message=prefix^t.error_message}leterror_range{error_range;_}=error_rangeleterror_message{error_message;_}=error_messageleterror_brief{error_brief;_}=error_briefletis_rendered{is_rendered;_}=is_renderedend(** A type of parser that has locations and error text for semantic errors. *)moduletypeLOCATED_STRING_SEMANTIC_PARSER=sigtypettypefinaltypesemantic=Semantic.ttypeexpect=string*Fmlib_parse.Indent.expectationoptionvalposition:t->Fmlib_parse.Position.tvalhas_succeeded:t->boolvalhas_failed_semantic:t->boolvalhas_failed_syntax:t->boolvalfailed_semantic:t->semanticvalfailed_expectations:t->expectlistvalfinal:t->finalendletpp_list_of_strings=Format.(pp_print_list~pp_sep:pp_print_cutpp_print_string)letwrite_pretty_print_to_string(r:Fmlib_pretty.Print.t):string=letopenFmlib_pretty.Printinletbuf=Buffer.create100inletrecwriter=ifhas_morerthenbeginBuffer.add_charbuf(peekr);write(advancer)endinwriter;Buffer.contentsbufletstring_find(f:char->bool)(i:int)(s:string):int=letl=String.lengthsinletrecloopi=ifi>=lthenlelseiffs.[i]thenielseloop(i+1)inloopimoduletypeOBSERVER_RESULT=sigtypetvalcreate_report:?code:string->?is_error:bool->unit->tvalwith_message:string->t->tvaladd_marker:marker_message:string->origin:stringoption->range:Fmlib_parse.Position.range->t->tvaladd_expectation:label:string->string->t->tvaladd_note:string->t->tvaladd_hint:string->t->tvalrender:origin:stringoption->source:string->t->stringmoduleMake:functor(P:LOCATED_STRING_SEMANTIC_PARSER)->sigvalobserve:cant_do:string->source:string->State.t->P.t->(P.final,Semantic.t)result(** [observe ~cant_do ~source state parser].
[cant_do] should say what couldn't be done if an error occurred. It must
be in ["VERB NOUN"] form like ["create database"] where the
["Could not"] start of sentence is implicit. *)endend(** A quick way to report a single error. *)letsingle_error~code~msg~brief_instruction(moduleResultObserver:OBSERVER_RESULT)sourcestaterangeplus=letorigin=State.originsourcestateinletis_error=not(State.downgrade_errors_into_warningssourcestate)inletro=ResultObserver.create_report~code~is_error()inletro=ResultObserver.with_messagemsgroinletro=ResultObserver.add_marker~marker_message:brief_instruction~origin~range:(ThunkRanges.inner_rangerangeplus)roinmatchState.source_contentssourcestatewith|None->msg|Somesource->ResultObserver.render~origin~sourceroopenstructletbrief_syntax~cant_do_sentence~failed_expectations()=matchfailed_expectationswith|[]->cant_do_sentence|[single]->Printf.sprintf"%s Expected: %s"cant_do_sentence(fstsingle)|_->Printf.sprintf"%s Expected one of:\n%s"cant_do_sentence(String.concat"\n"(List.map(functions,_->"- "^s)failed_expectations))letbrief_semantic~cant_do_sentence~failed_semantic()=cant_do_sentence^"\n"^Semantic.error_brieffailed_semanticend(** An observe result that uses the fmlib_parser [Error_reporter] library for
errors. It can do layout but not print in color. *)moduleMakeObserverWithErrorReporter:OBSERVER_RESULT=structtypemarker={message:string;origin:stringoption;range:Fmlib_parse.Position.range;}typeexpectation={label:string;explanation:string}typeblurb=Expectationofexpectation|Hintofstring|Noteofstringtypet={code:stringoption;message:stringoption;markers:markerlist;blurbs:blurblist;is_error:bool;}moduleDocCombinators(Doc:sigtypedocvalempty:docvaltext:string->docvalspace:docvalsubstring:string->int->int->docvalchar:char->docval(<+>):doc->doc->docval(>>):doc->(unit->doc)->docend)=structletdoc_of_range~origin(locrange:Fmlib_parse.Position.rangeoption):Doc.doc=letopenDocinmatch(locrange,origin)with|None,None->empty|None,Someorigin->text"In "<+>textorigin<+>text":"|Somelocrange,_->text"In "<+>text(Format.asprintf"%a"(ThunkRanges.pp_rangeorigin)locrange)<+>text":"<+>space(** Like {!Fmlib_pretty.Print.wrap_words} but breaks at line endings rather
than word endings. *)letwrap_pretty_print_lines(s:string):Doc.doc=letopenDocinletis_newlinec=c='\n'||c='\r'inletnot_newlinec=not(is_newlinec)inletline_starti=string_findnot_newlineisandline_endi=string_findis_newlineisandlen=String.lengthsinletrecfromi()=assert(i<len&¬_newlines.[i]);letj=line_endiinletk=line_startjinassert(i<j);assert(j=len||is_newlines.[j]);assert(k=len||not_newlines.[k]);(* A cryptic comment in [Fmlib_pretty.Print.paragraphs] says:
> The function works best if each paragraph ends in a newline.
So we add the newline (<+> char '\n') except at end of document.
But that produces extra newlines when [<+> group space], so
disable the group space.
With just the group space, we sometimes get space breaks rather
than line breaks, and that messes up the layout of nested
semantic errors where the source code has to be lined up with
the error. *)letd=substringsi(j-i)inifk=lenthen(* only newlines after [d] *)delsed<+>char'\n'(* <+> group space *)>>fromkinleti=line_start0inifi=lenthenemptyelsefromi()endmodulePrintCombinators=DocCombinators(Fmlib_pretty.Print)modulePrettyCombinators=DocCombinators(structincludeFmlib_pretty.Prettytypedoc=tlet(>>)(m:t)(f:unit->t):t=m<+>f()end)letdoc_to_string~origin(locrange:Fmlib_parse.Position.rangeoption)(doc:Fmlib_pretty.Print.doc):string=letopenFmlib_pretty.Printinletfirstline=PrintCombinators.doc_of_range~originlocrangeinwrite_pretty_print_to_string@@Fmlib_pretty.Print.layout70(group(firstline<+>doc))letpretty_to_string~origin(locrange:Fmlib_parse.Position.rangeoption)(pretty:Fmlib_pretty.Pretty.t):string=letopenFmlib_pretty.Prettyinletfirstline=PrettyCombinators.doc_of_range~originlocrangeinFmlib_pretty.Pretty.to_string(Fmlib_pretty.Pretty.layout70(group(firstline<+>pretty)))letcreate_report?code?(is_error=true)()={code;message=None;markers=[];blurbs=[];is_error}letwith_messagemessaget={twithmessage=Somemessage}letadd_marker~marker_message~origin~ranget=letmarker={message=marker_message;origin;range}in{twithmarkers=marker::t.markers}letadd_expectation~labelexplanationt=letexpectation={label;explanation}in{twithblurbs=Expectationexpectation::t.blurbs}letadd_hinthintt={twithblurbs=Hinthint::t.blurbs}letadd_notenotet={twithblurbs=Notenote::t.blurbs}letrender~origin~source{code;message;markers;blurbs;is_error}=letopenFmlib_pretty.Printinletwrap_liness=Stringext.split~on:'\n's|>List.map(funline->textline)|>paragraphsinletwith_idxl=List.mapi(funix->(i,x))linletdoc=fill20'-'<+>text(ifis_errorthen" FATAL ERROR "else" WARNING ")<+>fill20'-'<+>space<+>(matchcodewith|None->empty|Somecode->text(Printf.sprintf"[%s %s]"(ifis_errorthen"error"else"warning")code)<+>space)<+>matchmessagewithNone->empty|Somem->wrap_linesminletdoc=doc<+>space<+>fill20'.'in(* Either display [source] immediately (when there are no markers) *)letdoc=ifmarkers=[]thendoc<+>space<+>wrap_linessourceelsedocin(* Or display [source] within the first marker *)letdoc=List.fold_left(fundoc(i,{message;origin;range})->doc<+>space<+>PrintCombinators.doc_of_range~origin(Somerange)<+>(ifi=0thennest2(wrap_linessource)<+>spaceelseempty)<+>(text"-"<+>space<+>nest2(wrap_linesmessage)))doc(with_idxmarkers)inletdoc=(ifblurbs<>[]thenspaceelseempty)<+>List.fold_left(fundoc(i,blurb)->doc<+>space<+>(ifi=0thenfill20'.'<+>spaceelseempty)<+>matchblurbwith|Expectation{label;explanation}->textlabel<+>space<+>group(wrap_wordsexplanation<+>cut)|Hinthint->text"Hint:"<+>space<+>nest2(wrap_wordshint<+>cut)|Notenote->text"Note:"<+>space<+>nest2(wrap_wordsnote<+>cut))doc(with_idxblurbs)inletrendered=doc_to_string~originNonedocinThunkStrings.trim_lines_rightrenderedmoduleMake(P:LOCATED_STRING_SEMANTIC_PARSER)=structmoduleReporter=Fmlib_parse.Error_reporter.Make(P)letobserve~cant_do~sourcesourcestatep:(P.final,Semantic.t)result=letorigin=State.originsourcestateinletcant_do_sentence=Printf.sprintf"Could not %s."cant_doinifP.has_succeededpthenOk(P.finalp)elseifP.has_failed_syntaxpthen(* A syntax error is at one position. *)letrange=(P.positionp,P.positionp)inError(Semantic.create_rendered~brief:(brief_syntax~cant_do_sentence~failed_expectations:(P.failed_expectationsp)())range@@Reporter.(make_syntaxp|>run_on_stringsource|>pretty_to_string~origin(Somerange)))elseifP.has_failed_semanticpthen(* A semantic error is over a location range. *)letsemantic=P.failed_semanticpinletrange=Semantic.error_rangesemanticinifSemantic.is_renderedsemanticthen(* If the error has already been rendered, we don't wrap
and re-render it again. But we do prepend the original
"can't do" reason since it may have new debugging
information. And we say "warning" since it is not the root cause;
in fact it is a more general error. *)Error(Semantic.create_rendered~brief:(Semantic.error_briefsemantic)range@@Printf.sprintf"[warning] %s\n%s"cant_do_sentence(Semantic.error_messagesemantic))elseError(Semantic.create_rendered~brief:(brief_semantic~cant_do_sentence~failed_semantic:semantic())range@@Reporter.(makeSemantic.error_range(funsemantic'->PrettyCombinators.wrap_pretty_print_lines(Printf.sprintf"%s\n%s"cant_do_sentence(Semantic.error_messagesemantic')))p|>run_on_stringsource|>pretty_to_string~origin(Somerange)))elseError(Semantic.create_rendered~brief:cant_do_sentenceFmlib_parse.Position.(start,start)cant_do_sentence)endend(** An observe result that uses the Haskell [diagnose] library (an OCaml port)
for errors. It can print in color but doesn't do layout. *)moduleMakeObserverWithDiagnoseErrors(AnsiStyle:Diagnose.Diagnose.ANSI_STYLE):OBSERVER_RESULT=structmoduleDoc=Diagnose.Diagnose.MakeAnnotatedDoc(AnsiStyle)moduleThemes=Diagnose.Diagnose.MakeThemes(AnsiStyle)moduleReport=Diagnose.Diagnose.MakeReport(AnsiStyle)(Doc)(structletstyle=Themes.default_styleend)typet=stringReport.tletcreate_report?code?(is_error=true)():t={markers=[];is_error;code;message="TODO - fill in message";blurbs=[];}letto_markermsgorigin((start_,end_):Fmlib_parse.Position.range):Report.position*'msgReport.marker=letpos=letopenReportin{file=origin;begin_line=1+Fmlib_parse.Position.linestart_;end_line=1+Fmlib_parse.Position.lineend_;begin_col=1+Fmlib_parse.Position.columnstart_;end_col=1+Fmlib_parse.Position.columnend_;}in(pos,Thismsg)letadd_marker~marker_message~origin~range(report:t)={reportwithmarkers=to_markermarker_messageoriginrange::report.markers;}letwith_messagemessage(report:t)={reportwithmessage}letadd_expectation~labelexplanation(report:t)={reportwithblurbs=Report.Expectation(label,explanation)::report.blurbs;}letadd_hinthint(report:t)={reportwithblurbs=Report.Hinthint::report.blurbs}letadd_notenote(report:t)={reportwithblurbs=Report.Notenote::report.blurbs}letrender~origin~source(report:t):string=letreadonly_file_map=letline_array=String.split_on_char'\n'source|>Array.of_listinDiagnose.Diagnose.FilenameMap.addoriginline_arrayDiagnose.Diagnose.FilenameMap.emptyinletrendered=Report.pretty_report~readonly_file_map~with_unicode:true~tab_size:4reportinThunkStrings.trim_lines_rightrenderedletmk_pretty_reportsourcestatesource(report_transformer:stringoption->t->t)=letorigin=State.originsourcestateinletdowngrade_errors_into_warnings=State.downgrade_errors_into_warningssourcestateinrender~origin~source(report_transformerorigin(create_report~is_error:(notdowngrade_errors_into_warnings)()))moduleMake(P:LOCATED_STRING_SEMANTIC_PARSER)=structletobserve~cant_do~sourcesourcestatep:(P.final,Semantic.t)result=letcant_do_sentence=Printf.sprintf"Could not %s."cant_doinifP.has_succeededpthenOk(P.finalp)elseifP.has_failed_syntaxpthen(* A syntax error is at one position. *)letpos=P.positionpinletpretty_report=letfailures=P.failed_expectationspinmk_pretty_reportsourcestatesource(funoriginreport->letreport=with_message"There was a syntax error."reportinletreport=add_marker~marker_message:"This is invalid syntax."~origin~range:(pos,pos)reportinlet_i,report=List.fold_left(fun(i,report)(exp,_indent_expectation)->letreport=add_expectation~label:(ifi==0then"We expected:"else"or:")expreportin(i+1,report))(0,report)failuresinreport)inError(Semantic.create_rendered~brief:(brief_syntax~cant_do_sentence~failed_expectations:(P.failed_expectationsp)())(pos,pos)pretty_report)elseifP.has_failed_semanticpthen(* A semantic error is over a location range. *)letsemantic=P.failed_semanticpinletrange=Semantic.error_rangesemanticinifSemantic.is_renderedsemanticthen(* If the error has already been rendered, we don't wrap
and re-render it again. But we do prepend the original
"can't do" reason since it may have new debugging
information. And we say "warning" since it is not the root cause;
in fact it is a more general error. *)Error(Semantic.create_rendered~brief:(Semantic.error_briefsemantic)range@@Printf.sprintf"[warning] %s\n%s"cant_do_sentence(Semantic.error_messagesemantic))elseletpretty_report=mk_pretty_reportsourcestatesource(funoriginreport->letreport=with_messagecant_do_sentencereportinletreport=add_marker~marker_message:(Semantic.error_messagesemantic)~origin~rangereportinreport)inError(Semantic.create_rendered~brief:(brief_semantic~cant_do_sentence~failed_semantic:semantic())rangepretty_report)elseError(Semantic.create_rendered~brief:cant_do_sentenceFmlib_parse.Position.(start,start)cant_do_sentence)endend