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
|
(** 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.pp_print_list ~pp_sep:( fun ppf () ->
Format.pp_print_string ppf ".") Format.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_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_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_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
|