123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322openBaseopen!PpxlibopenAst_builder.Default(* ============================================================ *)(* Hole_key: identifies a hole in a format string *)(* ============================================================ *)moduleHole_key=structmoduleT=structtypet=|Numberofint|Labelofstring[@@derivingsexp_of,compare~localize]endincludeTincludeComparator.Make(T)letto_string=function|Numbern->Int.to_stringn|Labels->s;;letto_string_hum~total_count=function|Number1whentotal_count=1->"_"|Numbern->Int.to_stringn|Labels->s;;letvar_namet="__arg_"^to_stringtletlabel=function|Number_->None|Labels->Somes;;end(* ============================================================ *)(* Part: a parsed piece of a format string *)(* ============================================================ *)modulePart=structtypet=|Literalofstring|HoleofHole_key.tend(* ============================================================ *)(* Step 1: Parse the format string into parts *)(* ============================================================ *)typet={text:string;loc:Location.t}letadd_literalstring~start~until~acc=ifstart>=untilthenaccelse`Literal(String.substring~pos:start~len:(until-start))::acc;;letvalidate_field_nameraw_content~loc=letname=String.stripraw_contentinifnot(String.for_allname~f:(func->Char.is_alphanumc||Char.equalc'_'))thenLocation.Error.raise(Location.Error.createf~loc"invalid field name: %S"name);name;;(* Find the next special sequence starting at [pos]: either [%%] (escape) or [%{]
(interpolation). Returns [None] if no special sequence is found.
A lone [%] (not followed by [%] or [{]) is treated as a literal character and does not
need escaping. Only [%{] has special meaning; [%%] is the escape for a literal [%]
before [{]. *)letfind_next_specialstring~pos=letlen=String.lengthstringinletrecscani=ifi+1>=lenthenNoneelseifChar.equalstring.[i]'%'then(matchstring.[i+1]with|'%'->Some(`Escape,i)|'{'->Some(`Interp,i)|_->scan(i+1))elsescan(i+1)inscanpos;;letparse_interpolationstring~loc~content_start=matchString.substr_indexstring~pos:content_start~pattern:"}"with|None->Location.raise_errorf~loc"unterminated %%{"|Someclose_pos->letraw_content=String.substring~pos:content_start~len:(close_pos-content_start)inletname=validate_field_nameraw_content~locinclose_pos+1,name;;(** Parse the format string into a flat list of literals and raw (unclassified) hole
names. Holes appear as [`Hole name] where [name] is the raw content between [%{] and
[}]. *)letrecparse_rawstring~loc~pos~acc=letlen=String.lengthstringinmatchfind_next_specialstring~poswith|None->letacc=add_literalstring~start:pos~until:len~accinList.revacc|Some(`Escape,esc_pos)->letacc=add_literalstring~start:pos~until:esc_pos~accinletacc=`Literal"%"::accinparse_rawstring~loc~pos:(esc_pos+2)~acc|Some(`Interp,interp_pos)->letacc=add_literalstring~start:pos~until:interp_pos~accinletnext_pos,name=parse_interpolationstring~loc~content_start:(interp_pos+2)inletacc=`Holename::accinparse_rawstring~loc~pos:next_pos~acc;;(** Classify raw parts into [Part.t]: literal strings pass through, and raw hole names are
resolved to [Hole_key.t] values ([_] and bare numbers become [Number], everything else
becomes [Label]). Validates numbering constraints (1-indexed, in range). *)letclassifyraw_parts~loc=letclassified=List.mapraw_parts~f:(function|`Literals->`Literals|`Hole"_"->`Hole`Underscore|`Holes->(matchInt.of_string_optswith|Somen->ifn<1thenLocation.raise_errorf~loc"Numbered holes are 1-indexed and must be positive, got: %n"nelse`Hole(`Numberedn)|None->`Hole(`Nameds)))inletunnamed_hole_count=List.countclassified~f:(function|`Hole(`Underscore|`Numbered_)->true|`Literal_|`Hole(`Named_)->false)inList.mapclassified~f:(function|`Literals->Part.Literals|`Hole`Underscore->ifunnamed_hole_count>1thenLocation.raise_errorf~loc"Cannot use %%{_} if there's more than one unnamed hole, found %d such holes."unnamed_hole_countelseHole(Number1)|`Hole(`Numberedn)->ifn>unnamed_hole_countthenLocation.raise_errorf~loc"Used %d in a numbered hole, but there %s only %d numbered %s."n(ifunnamed_hole_count=1then"is"else"are")unnamed_hole_count(ifunnamed_hole_count=1then"hole"else"holes")elseHole(Numbern)|`Hole(`Namedname)->Hole(Labelname));;(* Parse the format string into a list of [Part.t] with classified [Hole_key.t] values.
Raises on parse errors and invalid hole names. *)letparse{text;loc}=parse_rawtext~loc~pos:0~acc:[]|>classify~loc(* ============================================================ *)(* Step 2: Match parsers with holes *)(* ============================================================ *)(* Assign [Hole_key.t] values to each entry: [None] entries are numbered 1, 2, ... in list
order; [Some name] entries become [Label name]. *)lethole_keys_of_listparsers=List.folding_mapparsers~init:0~f:(funnumbered_count(label,expr)->matchlabelwith|None->letn=numbered_count+1inn,(Hole_key.Numbern,expr)|Somename->numbered_count,(Hole_key.Labelname,expr));;(* Build a map from [Hole_key.t] to [expression] by matching the caller's expressions
against the holes that appear in the format string.
Raises on duplicate holes, missing expressions, or extra expressions. *)letvalidate_holes~parts~parser_map~loc=lethole_key_set=lethole_keys=List.filter_mapparts~f:(function|Part.Holekey->Somekey|Literal_->None)inmatchList.find_all_dupshole_keys~compare:Hole_key.comparewith|[]->Set.of_list(moduleHole_key)hole_keys|_::_asdups->letdups=List.mapdups~f:Hole_key.to_string|>String.concat~sep:", "inLocation.raise_errorf~loc"The following holes have been specified more than once: %s."dupsin(* Validate: every parser has a corresponding hole in the format string *)Map.iter_keysparser_map~f:(funkey->ifnot(Set.memhole_key_setkey)thenLocation.raise_errorf~loc"The format string is expected to contain the hole %%{%s} but no such hole found"(Hole_key.to_string_hum~total_count:(Map.lengthparser_map)key));(* Validate: every hole in the format string has a corresponding parser *)Set.iterhole_key_set~f:(funkey->ifnot(Map.memparser_mapkey)thenLocation.raise_errorf~loc"The hole %%{%s} inside the format string does not correspond to any field"(Hole_key.to_string_hum~total_count:(Set.lengthhole_key_set)key));;(* ============================================================ *)(* Step 3: Build the expression *)(* ============================================================ *)moduleFields=structtypet=|None|Singletonof{parser:expression}|Manyof{parsers:(stringoption*expression)list;combine:(stringoption*expression)list->expression}endletbuild_expression_none~parts~loc=letparser_exprs=List.mapparts~f:(function|Part.Literaltext->[%exprUnstring.lit[%eestring~loctext]]|Hole_->failwith"ppx_unstring bug: no holes expected")in[%exprUnstring.all_unit[%eelist~locparser_exprs]];;letbuild_expression_singleton~parts~parser~loc=letvar_name=Hole_key.var_name(Number1)inletparser_exprs,patterns=List.mapparts~f:(function|Part.Literaltext->[%exprUnstring.lit[%eestring~loctext]],[%pat?()]|Hole_->parser,pvar~locvar_name)|>List.unzipinmatchparser_exprswith|[parser]->parser|_->[%exprUnstring.map_hlist[%eelist~locparser_exprs]~f:(fun[%pplist~locpatterns]->[%eevar~locvar_name])];;letbuild_expression_many~parts~parser_map~caller_parsers~combine~loc=letparser_exprs,patterns=List.mapparts~f:(function|Part.Literaltext->[%exprUnstring.lit[%eestring~loctext]],[%pat?()]|Holekey->Map.find_exnparser_mapkey,pvar~loc(Hole_key.var_namekey))|>List.unzipinlethole_args=List.mapcaller_parsers~f:(fun(key,_)->Hole_key.labelkey,evar~loc(Hole_key.var_namekey))in[%exprUnstring.map_hlist[%eelist~locparser_exprs]~f:(fun[%pplist~locpatterns]->[%ecombinehole_args])];;(* ============================================================ *)(* build_unstring: public entry point *)(* ============================================================ *)letbuild_string({loc=string_loc;_}ast)~loc~hole_string_exprs=letparts=parsetinletcaller_exprs=hole_keys_of_listhole_string_exprsinletexpr_map=Map.of_alist_exn(moduleHole_key)caller_exprsinvalidate_holes~parts~parser_map:expr_map~loc:string_loc;letstring_parts=List.mapparts~f:(function|Part.Literaltext->estring~loctext|Part.Holekey->Map.find_exnexpr_mapkey)inmatchstring_partswith|[]->estring~loc""|[single]->single|_->[%exprString.concat~sep:""[%eelist~locstring_parts]];;letbuild_unstring({loc=string_loc;_}ast)~loc~fields=letparts=parsetinmatch(fields:Fields.t)with|None->validate_holes~parts~parser_map:(Map.empty(moduleHole_key))~loc:string_loc;build_expression_none~parts~loc|Singleton{parser}->letparser_map=Map.singleton(moduleHole_key)(Number1)parserinvalidate_holes~parts~parser_map~loc:string_loc;build_expression_singleton~parts~parser~loc|Many{parsers;combine}->letcaller_parsers=hole_keys_of_listparsersinletparser_map=Map.of_alist_exn(moduleHole_key)caller_parsersinvalidate_holes~parts~parser_map~loc:string_loc;build_expression_many~parts~parser_map~caller_parsers~combine~loc;;