summaryrefslogtreecommitdiff
path: root/manual/tests/cross_reference_checker.ml
blob: c11f40c24b64e2a7757606f5610a1d3da3e530c0 (plain)
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