1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
|
(** Check reference to manual section in ml files
[cross-reference-checker -auxfile tex.aux src.ml ]
checks that all expression and let bindings in [src.ml] annotated
with [[@manual.ref "tex_label"]] are integer tuple literals or
lists, e.g
{[
let[@manual.ref "sec:major"] ref = 1, 1
(* or *)
let[@manual.ref "sec:major"] ref = [ 1; 1]
(* or *)
let ref = (3 [@manual.ref "ch:pentatonic"])
]}
and that their values are consistent with the computed references for the
payload labels (e.g "sec:major", "ch:pentatonic") present in the TeX
auxiliary file [tex.aux]
*)
(** {1 Error printing } *)
type error =
| Reference_mismatch of
{loc:Location.t; label:string; ocaml:int list; tex:int list}
| Unknown_label of Location.t * string
| Tuple_or_list_expected of Location.t
| No_aux_file
| Wrong_attribute_payload of Location.t
let pp_ref ppf = Format_doc.pp_print_list ~pp_sep:( fun ppf () ->
Format_doc.pp_print_string ppf ".") Format_doc.pp_print_int ppf
let print_error error =
Location.print_report Format.std_formatter @@ match error with
| Tuple_or_list_expected loc ->
Location.errorf ~loc
"Integer tuple or list expected after manual reference annotation@."
| Unknown_label (loc,label) ->
Location.errorf ~loc
"@[<hov>Unknown manual label:@ %s@]@." label
| Reference_mismatch r ->
Location.errorf ~loc:r.loc
"@[<v 2>References for label %S do not match:@,\
OCaml side %a,@,\
manual %a@]@."
r.label
pp_ref r.ocaml
pp_ref r.tex
| No_aux_file ->
Location.errorf "No aux file provided@."
| Wrong_attribute_payload loc ->
Location.errorf ~loc "Wrong payload for \"@manual.ref\"@."
(** {1 Main types} *)
(** Maps of ocaml reference to manual labels *)
module Refs = Map.Make(String)
(** Reference extracted from TeX aux files *)
type tex_reference =
{ label: string;
pos: int list;
level: string
}
type status = Ok | Bad | Unknown
(** Reference extracted from OCaml source files *)
type ml_reference = { loc: Location.t; pos: int list; status:status }
(** {1 Consistency check } *)
let check_consistency (ref:tex_reference) {loc; pos; _ } =
if ref.pos = pos then
{ loc; pos; status = Ok }
else begin
print_error @@ Reference_mismatch {loc;label=ref.label;tex=ref.pos;ocaml=pos};
{loc; pos; status = Bad }
end
let rec check_final_status label error = function
| { status = Ok; _ } -> error
| { status = Bad; _ } -> true
| { status = Unknown; loc; _} ->
print_error (Unknown_label (loc,label));
true
(** {1 Data extraction from TeX side} *)
module TeX = struct
(** Read reference information from a line of the aux file *)
let scan s =
try
Scanf.sscanf s
"\\newlabel{%s@}{{%s@}{%_d}{%_s@}{%s@.%_s@}{%_s@}}"
(fun label position_string level ->
let pos =
List.map int_of_string (String.split_on_char '.' position_string) in
Some {label;level;pos} )
with
| Scanf.Scan_failure _ -> None
| Failure _ -> None
let check_line refs line =
match scan line with
| None -> refs
| Some ref ->
match Refs.find_opt ref.label refs with
| None -> refs
| Some l ->
Refs.add ref.label
(List.map (check_consistency ref) l)
refs
let check_all aux refs =
let chan = open_in aux in
let rec lines refs =
let s = try Some (input_line chan) with End_of_file -> None in
match s with
| None -> refs
| Some line ->
lines @@ check_line refs line in
let refs = lines refs in
close_in chan;
let error = Refs.fold (fun label ocaml_refs error ->
List.fold_left (check_final_status label) error ocaml_refs)
refs false in
if error then exit 2 else exit 0
end
(** {1 Extract references from Ocaml source files} *)
module OCaml_refs = struct
let parse sourcefile =
Pparse.parse_implementation ~tool_name:"manual_cross_reference_check"
sourcefile
(** search for an attribute [[@manual.ref "tex_label_name"]] *)
let manual_reference_attribute attr =
let open Parsetree in
if attr.attr_name.Location.txt <> "manual.ref"
then None
else begin match attr.attr_payload with
| PStr [{pstr_desc= Pstr_eval
({ pexp_desc = Pexp_constant
{ pconst_desc = Pconst_string (s,_,_); _ } },_) } ] ->
Some s
| _ -> print_error (Wrong_attribute_payload attr.attr_loc);
Some "" (* triggers an error *)
end
let rec label_from_attributes = function
| [] -> None
| a :: q -> match manual_reference_attribute a with
| Some _ as x -> x
| None -> label_from_attributes q
let int e =
let open Parsetree in
match e.pexp_desc with
| Pexp_constant { pconst_desc = Pconst_integer (s, _ ); _ } ->
int_of_string s
| _ -> raise Exit
let int_list l =
try Some (List.map int l) with
| Exit -> None
(** We keep a list of OCaml-side references to the same label *)
let add_ref label ref refs =
let l = match Refs.find_opt label refs with
| None -> [ref]
| Some l -> ref :: l in
Refs.add label l refs
let rec try_parse_as_list e =
match e.Parsetree.pexp_desc with
| Parsetree.Pexp_construct
({ txt = Lident "::"; _ }, Some { pexp_desc = Pexp_tuple [ x; rest]; _ }) ->
((int x) :: try_parse_as_list rest)
| Parsetree.Pexp_construct ({ txt = Lident "[]"; _}, None) ->
[]
| _ -> raise Exit
let list_expression e =
try Some (try_parse_as_list e) with | Exit -> None
let inner_expr loc e =
let tuple_expected () = print_error (Tuple_or_list_expected loc) in
match e.Parsetree.pexp_desc with
| Parsetree.Pexp_tuple l ->
begin match int_list l with
| None -> tuple_expected (); []
| Some pos -> pos
end
| Parsetree.Pexp_constant { pconst_desc = Pconst_integer (n,_); _ } ->
[int_of_string n]
| _ ->
begin match list_expression e with
| Some list -> list
| None -> tuple_expected (); []
end
(** extract from [let[@manual.ref "label"] x= 1, 2] *)
let value_binding m iterator vb =
let open Parsetree in
begin match label_from_attributes vb.pvb_attributes with
| None -> ()
| Some label ->
let pos = inner_expr vb.pvb_loc vb.pvb_expr in
m := add_ref label {loc = vb.pvb_loc; pos; status = Unknown } !m
end;
iterator.Ast_iterator.expr iterator vb.pvb_expr
(** extract from [ (1,2)[@manual.ref "label"]] *)
let expr m iterator e =
let open Parsetree in
begin match label_from_attributes e.pexp_attributes with
| None -> ()
| Some label ->
let pos = inner_expr e.pexp_loc e in
m := add_ref label {loc = e.pexp_loc; pos; status = Unknown } !m
end;
Ast_iterator.default_iterator.expr iterator e
let from_ast m ast =
let iterator =
let value_binding = value_binding m in
let expr = expr m in
Ast_iterator.{ default_iterator with value_binding; expr } in
iterator.structure iterator ast
let from_file m f =
from_ast m @@ parse f
end
(** {1 Argument handling and main function } *)
let usage =
"cross-reference-check -auxfile [file.aux] file_1 ... file_n checks that \
the cross reference annotated with [@manual_cross_reference] are consistent \
with the provided auxiliary TeX file"
(** the auxiliary file containing reference to be checked *)
let aux_file = ref None
let args =
[
"-auxfile",Arg.String (fun s -> aux_file := Some s),
"set the reference file"
]
let () =
let m = ref Refs.empty in
Arg.parse args (OCaml_refs.from_file m) usage;
match !aux_file with
| None -> print_error No_aux_file; exit 2
| Some aux ->
let error = TeX.check_all aux !m in
if error then exit 2 else exit 0
|