package mdx

  1. Overview
  2. Docs

Source file block.ml

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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
(*
 * Copyright (c) 2018 Thomas Gazagnaire <thomas@gazagnaire.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *)

open Astring

type section = int * string

type cram_value = { pad: int; tests: Cram.t list }

type value =
  | Raw
  | OCaml
  | Error of string list
  | Cram of cram_value
  | Toplevel of Toplevel.t list

type t = {
  line    : int;
  file    : string;
  section : section option;
  labels  :
    (string * ([`Eq | `Neq | `Le | `Lt | `Ge | `Gt] * string) option) list;
  header  : string option;
  contents: string list;
  value   : value;
}

let empty = {
  line=0;
  file="";
  section=None;
  labels=[];
  header=None;
  contents=[];
  value=Raw
}

let dump_string ppf s = Fmt.pf ppf "%S" s
let dump_section = Fmt.(Dump.pair int string)

let dump_value ppf = function
  | Raw -> Fmt.string ppf "Raw"
  | OCaml -> Fmt.string ppf "OCaml"
  | Error e -> Fmt.pf ppf "Error %a" Fmt.(Dump.list dump_string) e
  | Cram { pad; tests } ->
    Fmt.pf ppf "@[Cram@ {pad=%d;@ tests=%a}@]"
      pad Fmt.(Dump.list Cram.dump) tests
  | Toplevel tests ->
    Fmt.pf ppf "@[Toplevel %a@]" Fmt.(Dump.list Toplevel.dump) tests

let dump_relation ppf = function
  | `Eq -> Fmt.string ppf "="
  | `Neq -> Fmt.string ppf "<>"
  | `Gt -> Fmt.string ppf ">"
  | `Ge -> Fmt.string ppf ">="
  | `Lt -> Fmt.string ppf "<"
  | `Le -> Fmt.string ppf "<="

let dump_labels =
  Fmt.(Dump.(list (pair dump_string (option (pair dump_relation dump_string)))))

let dump ppf { file; line; section; labels; header; contents; value } =
  Fmt.pf ppf
    "{@[file: %s;@ line: %d;@ section: %a;@ labels: %a;@ header: %a;@
        contents: %a;@ value: %a@]}"
    file line
    Fmt.(Dump.option dump_section) section
    dump_labels labels
    Fmt.(Dump.option string) header
    Fmt.(Dump.list dump_string) contents
    dump_value value

let pp_lines syntax =
  let pp =
    match syntax with
    | Some Syntax.Cram -> Fmt.fmt "  %s"
    | _ -> Fmt.string
  in
  Fmt.(list ~sep:(unit "\n") pp)
let pp_contents ?syntax ppf t = Fmt.pf ppf "%a\n" (pp_lines syntax) t.contents
let pp_footer ?syntax ppf () =
  match syntax with
  | Some Syntax.Cram -> ()
  | _ -> Fmt.string ppf "```\n"

let pp_cmp ppf = function
  | `Eq -> Fmt.pf ppf "="
  | `Neq -> Fmt.pf ppf "<>"
  | `Lt -> Fmt.pf ppf "<"
  | `Le -> Fmt.pf ppf "<="
  | `Gt -> Fmt.pf ppf ">"
  | `Ge -> Fmt.pf ppf ">="

let pp_label ppf (k, v) = match v with
  | None   -> Fmt.string ppf k
  | Some (o, v) -> Fmt.pf ppf "%s%a%s" k pp_cmp o v

let pp_labels ppf = function
  | [] -> ()
  | l  -> Fmt.pf ppf " %a" Fmt.(list ~sep:(unit ",") pp_label) l

let pp_header ?syntax ppf t =
  match syntax with
  | Some Syntax.Cram ->
    assert (t.header = Syntax.cram_default_header);
    begin match t.labels with
    | [] -> ()
    | ["non-deterministic", Some (`Eq, choice)] ->
      Fmt.pf ppf "<-- non-deterministic %s\n" choice
    | _ ->
      let err =
        Fmt.strf "Block.pp_header: [ %a ]" pp_labels t.labels
      in
      invalid_arg err
    end
  | _ ->
    Fmt.pf ppf "```%a%a\n" Fmt.(option string) t.header pp_labels t.labels

let pp_error ppf b =
  match b.value with
  | Error e -> List.iter (fun e -> Fmt.pf ppf ">> @[<h>%a@]@." Fmt.words e) e
  | _ -> ()

let pp ?syntax ppf b =
  pp_header ?syntax ppf b;
  pp_error ppf b;
  pp_contents ?syntax ppf b;
  pp_footer ?syntax ppf ()

let labels = [
  `Label "dir"              , [`Any];
  `Label "source-tree"      , [`Any];
  `Label "file"             , [`Any];
  `Label "part"             , [`Any];
  `Label "env"              , [`Any];
  `Label "skip"             , [`None];
  `Label "non-deterministic", [`None; `Some "command"; `Some "output"];
  `Label "version"          , [`Any];
  `Prefix "set-"            , [`Any];
]

let pp_value ppf = function
  | `Any   -> Fmt.string ppf "*"
  | `None   -> Fmt.string ppf "<none>"
  | `Some v -> dump_string ppf v

let match_label l p = match p, l with
  | `Any   , Some _ -> true
  | `None  , None   -> true
  | `Some p, Some (_, l) -> String.equal p l
  | _ -> false

let pp_v ppf = function
  | None   -> Fmt.string ppf "<none>"
  | Some (_, v) -> Fmt.string ppf v

let rec pp_list pp ppf = function
  | []    -> ()
  | [x]   -> pp ppf x
  | [x;y] -> Fmt.pf ppf "%a and %a" pp x pp y
  | h::t  -> Fmt.pf ppf "%a, %a" pp h (pp_list pp) t

let check_labels t =
  List.fold_left (fun acc (k, v) ->
      try
        let f = function
        | `Label s, _ -> s = k
        | `Prefix s, _ -> String.equal (String.with_range ~len:(String.length s) k) s in
        let _, vs = List.find f labels in
        if List.exists (match_label v) vs then acc
        else
          Fmt.strf "%a is not a valid value for label %S. \
                    Valid values are %a."
            pp_v v k (pp_list pp_value) vs
          :: acc
      with Not_found ->
        let f = function
        | `Label _, _ -> true
        | _ -> false in
        let g = function `Label s, _ | `Prefix s, _ -> s in
        let ls, ps = List.partition f labels in
        Fmt.strf "%S is not a valid label or prefix. \
                  Valid labels are %a and valid prefixes are %a."
          k (pp_list dump_string) (List.map g ls)
          (pp_list dump_string) (List.map g ps)
        :: acc
    ) [] t.labels
  |> function
  | [] -> Result.Ok ()
  | es -> Result.Error es

let get_label t label =
  try Some (List.assoc label t.labels)
  with Not_found -> None

let get_labels t label =
  List.fold_left (fun acc (k, v) ->
      if String.equal k label then match v with
        | None   -> assert false
        | Some v -> v ::acc
      else acc
    ) [] t.labels

let get_prefixed_labels t prefix =
  List.fold_left (fun acc (k, v) ->
      if String.equal (String.with_range ~len:(String.length prefix) k) prefix then match v with
        | None   -> assert false
        | Some (e, s) -> (e, (String.with_range ~first:(String.length prefix) k, s)) ::acc
      else acc
    ) [] t.labels

let directory t = match get_label t "dir" with
  | None   -> None
  | Some None -> None
  | Some (Some (`Eq, d)) -> Some d
  | Some (Some _) -> Fmt.failwith "invalid `dir` label value"

let file t = match get_label t "file" with
  | None   -> None
  | Some None -> None
  | Some (Some (`Eq, f)) -> Some f
  | Some (Some _) -> Fmt.failwith "invalid `file` label value"

let part t = match get_label t "part" with
  | None   -> None
  | Some None -> None
  | Some (Some (`Eq, l)) -> Some l
  | Some (Some _) -> Fmt.failwith "invalid `part` label value"

let version t = match get_label t "version" with
  | Some (Some (op, v)) ->
    let x, y, z = Misc.parse_version v in
    op, x, y, z
  | _ -> `Eq, None, None, None

let source_trees t =
  let f = function
    | `Eq, x -> x
    | _ -> Fmt.failwith "invalid `source-tree` label value"
  in
  List.map f (get_labels t "source-tree")

let mode t = match get_label t "non-deterministic" with
  | None                  -> `Normal
  | Some None
  | Some (Some (`Eq, "output"))  -> `Non_det `Output
  | Some (Some (`Eq, "command")) -> `Non_det `Command
  | Some (Some (`Eq, _))         -> `Normal
  | Some (Some _) -> Fmt.failwith "invalid `non-deterministic` label value"

let skip t = match get_label t "skip" with
  | Some None -> true
  | _ -> false

let environment t = match get_label t "env" with
  | None
  | Some None
  | Some (Some (`Eq, "default")) -> "default"
  | Some (Some (`Eq, s)) -> s
  | Some (Some _) -> Fmt.failwith "invalid `env` label value"

let variables t =
  let f = function
    | `Eq, x -> x
    | _ -> Fmt.failwith "invalid env variable operator (use '=' only)"
  in
  List.map f (get_prefixed_labels t "set-")

let value t = t.value
let section t = t.section
let header t = t.header

let cram lines =
  let pad, tests = Cram.of_lines lines in
  Cram { pad; tests }

let guess_ocaml_kind b =
  let rec aux = function
    | []     -> `Code
    | h :: t ->
      let h = String.trim h in
      if h = "" then aux t
      else if String.length h > 1 && h.[0] = '#' then `Toplevel
      else `Code
  in
  match b.header, b.contents with
  | Some "ocaml", t -> `OCaml (aux t)
  | _ -> `Other

let toplevel ~file ~line lines = Toplevel (Toplevel.of_lines ~line ~file lines)

let eval t =
  match check_labels t with
  | Error e -> { t with value = Error e }
  | Ok ()   ->
    match t.header with
    | Some ("sh" | "bash") ->
      let value = cram t.contents in
      { t with value }
    | Some "ocaml" ->
      (match guess_ocaml_kind t with
       | `OCaml `Code -> { t with value = OCaml }
       | `OCaml `Toplevel ->
         let value = toplevel ~file:t.file ~line:t.line t.contents in
         { t with value }
       | `Other -> { t with value = Raw })
    | _ -> t

let ends_by_semi_semi c = match List.rev c with
  | h::_ ->
    let len = String.length h in
    len > 2 && h.[len-1] = ';' && h.[len-2] = ';'
  | _ -> false

let pp_line_directive ppf (file, line) = Fmt.pf ppf "#%d %S" line file
let line_directive = Fmt.to_to_string pp_line_directive

let executable_contents b =
  let contents =
    match guess_ocaml_kind b with
    | `OCaml `Code -> b.contents
    | `OCaml `Toplevel | `Other ->
      match b.value with
      | Error _ | Raw | Cram _ -> []
      | OCaml -> line_directive (b.file, b.line) :: b.contents
      | Toplevel tests ->
        List.flatten (
          List.map (fun t ->
              match Toplevel.command t with
              | [] -> []
              | cs ->
                let mk s = String.v ~len:(t.hpad+2) (fun _ -> ' ') ^ s in
                line_directive (b.file, t.line) :: List.map mk cs
            ) tests)
  in
  if contents = [] || ends_by_semi_semi contents then contents
  else contents @ [";;"]

let split sep s op =
  match String.cut ~sep s with
  | None -> s, None (* operator does not matter here *)
  | Some (k, v) -> k, Some (op, v)

let ( ||| ) x y =
  match x with
  | _, None -> y
  | x -> x

let labels_of_string s =
  let labels = String.cuts ~empty:false ~sep:"," s in
  List.map (fun s ->
      split "<>" s `Neq |||
      split ">=" s `Ge |||
      split ">"  s `Gt |||
      split "<=" s `Le |||
      split "<"  s `Lt |||
      split "="  s `Eq
    ) labels

let compare_versions v1 v2 =
  match (v1, v2) with
  | (Some _, Some _, Some _), (None, _, _) -> 0
  | (Some x, Some _, Some _), (Some x', None, _) -> x - x'
  | (Some x, Some y, Some _), (Some x', Some y', None) ->
    if x = x' then y - y' else x - x'
  | (Some x, Some y, Some z), (Some x', Some y', Some z') ->
    if x = x' then
      if y = y' then z - z'
      else y - y'
    else x - x'
  | _ -> Fmt.failwith "incomplete OCaml version"

let compare = function
  | `Eq -> ( = )
  | `Neq -> ( <> )
  | `Lt -> ( < )
  | `Le -> ( <= )
  | `Gt -> ( > )
  | `Ge -> ( >= )

let version_enabled t =
  let curr_version = Misc.parse_version Sys.ocaml_version in
  let op, x, y, z = version t in
  (compare op) (compare_versions curr_version (x, y, z)) 0