Source file parse.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
module W = Warnings

type 'a node = 'a W.node

let parse_string s =
  let is_ws idx = match s.[idx] with '\n' | ' ' -> true | _ -> false in
  let is_alpha idx =
    let c = s.[idx] in
    ('a' <= c && c <= 'z')
    || ('A' <= c && c <= 'Z')
    || ('0' <= c && c <= '9')
    || c = '_'
  in
  let rec consume_ws idx =
    if idx >= String.length s then idx
    else if is_ws idx then consume_ws (idx + 1)
    else idx
  in
  let rec consume_non_ws_non_semicolon idx =
    if idx >= String.length s then idx
    else if (not (is_ws idx)) && not (Char.equal s.[idx] ';') then
      consume_non_ws_non_semicolon (idx + 1)
    else idx
  in
  let rec consume_alpha idx =
    if idx >= String.length s then idx
    else if is_alpha idx then consume_alpha (idx + 1)
    else idx
  in
  let quoted_string idx0 =
    let rec take_inside_quoted_string acc idx =
      if idx >= String.length s then failwith "Missing end of quote"
      else
        match s.[idx] with
        | '"' ->
            ( (acc |> List.rev |> List.to_seq |> String.of_seq, (idx0, idx - 1)),
              idx + 1 )
        | '\\' ->
            if idx + 1 >= String.length s then
              failwith "Unterminated escape sequence in quoted string"
            else take_inside_quoted_string (s.[idx + 1] :: acc) (idx + 2)
        | _ -> take_inside_quoted_string (s.[idx] :: acc) (idx + 1)
    in
    take_inside_quoted_string [] idx0
  in
  let parse_unquoted_string idx =
    let idx0 = idx in
    let idx = consume_non_ws_non_semicolon idx in
    let arg = String.sub s idx0 (idx - idx0) in
    ((arg, (idx0, idx)), idx)
  in
  let parse_arg idx =
    match s.[idx] with
    | '"' -> quoted_string (idx + 1)
    | _ -> parse_unquoted_string idx
    | exception _ -> failwith ": needs something after"
  in
  let repeat parser idx =
    let rec do_ acc idx =
      match parser idx with
      | None -> (List.rev acc, idx)
      | Some (x, idx') ->
          if idx' = idx then
            failwith "Parser did not consume input; infinite loop detected"
          else do_ (x :: acc) idx'
    in
    do_ [] idx
  in
  let parse_name idx =
    let idx0 = idx in
    let idx = consume_alpha idx in
    let name = String.sub s idx0 (idx - idx0) in
    (name, idx)
  in
  let parse_column idx =
    match s.[idx] with
    | ':' -> idx + 1
    | _ -> failwith "no : after named argument"
    | exception _ -> failwith "no : after named argument"
  in
  let parse_named idx =
    let idx0 = consume_ws idx in
    match s.[idx0] with
    | '~' ->
        let idx = idx0 + 1 in
        let name, idx = parse_name idx in
        let () =
          if String.equal name "" then
            failwith "'~' needs to be followed by a name"
        in
        let name_loc = (idx0, idx) in
        let idx = parse_column idx in
        let arg, idx = parse_arg idx in
        Some (((name, name_loc), arg), idx)
    | (exception Invalid_argument _) | _ -> None
  in
  let parse_semicolon idx =
    let idx = consume_ws idx in
    match s.[idx] with
    | ';' -> Some ((), idx + 1)
    | (exception Invalid_argument _) | _ -> None
  in
  let parse_positional idx =
    let idx = consume_ws idx in
    match s.[idx] with
    | _ -> Some (parse_arg idx, idx)
    | exception Invalid_argument _ -> None
  in
  let parse_one idx =
    let ( let$ ) x f = match x with Some _ as x -> x | None -> f () in
    let ( let> ) x f = Option.map f x in
    let$ () =
      let> named, idx = parse_named idx in
      (`Named named, idx)
    in
    let$ () =
      let> (), idx' = parse_semicolon idx in
      (`Semicolon idx', idx')
    in
    let> (p, idx'), _idx = parse_positional idx in
    (`Positional p, idx')
  in
  let parse_all = repeat parse_one in
  let parsed, _ = parse_all 0 in
  let (unfinished_acc, loc), parsed =
    List.fold_left
      (fun ((current_acc, idx0), global_acc) -> function
        | `Semicolon idx ->
            (([], idx), (List.rev current_acc, (idx0, idx)) :: global_acc)
        | (`Positional _ | `Named _) as x ->
            ((x :: current_acc, idx0), global_acc))
      (([], 0), [])
      parsed
  in
  let parsed =
    (List.rev unfinished_acc, (loc, String.length s)) :: parsed |> List.rev
  in
  parsed
  |> List.map @@ fun (l, loc) ->
     ( List.partition_map
         (function `Named x -> Left x | `Positional p -> Right p)
         l,
       loc )

let ( let+ ) x y = Result.map y x

module Smap = Map.Make (String)

type action = {
  name : string;
  named : (string node * W.loc) Smap.t;
  positional : string node list;
}

let parse_string ~action_name s : (_ W.t, _) result =
  let+ s =
    try Ok (parse_string s) with
    | Failure s -> Error (`Msg s)
    | _ (* TODO: finer grain catch and better error messages *) ->
        Error (`Msg "Failed when trying to parse argument")
  in
  let res, warnings =
    s
    |> List.map (fun ((named, positional), loc) ->
           let named, warnings =
             named
             |> List.fold_left
                  (fun (map, warnings) ((k, k_loc), (v, loc')) ->
                    match Smap.find_opt k map with
                    | None -> (Smap.add k ((v, loc'), k_loc) map, warnings)
                    | Some _ ->
                        (* let loc = _ in *)
                        let msg =
                          "Named argument '" ^ k
                          ^ "' is duplicated. This instance is ignored."
                        in
                        let w = W.Parsing_failure { msg; loc = k_loc } in
                        (map, w :: warnings))
                  (Smap.empty, [])
           in
           (({ name = action_name; named; positional }, loc), warnings))
    |> List.split
  in
  let warnings = List.concat warnings in
  (res, warnings)

let id x = x

type 'a description_named_atom =
  string * (string node -> ('a, [ `Msg of string ]) result)

type _ descr_tuple =
  | [] : unit descr_tuple
  | ( :: ) : 'a description_named_atom * 'b descr_tuple -> ('a * 'b) descr_tuple

type _ output_tuple =
  | [] : unit output_tuple
  | ( :: ) : 'a option * 'b output_tuple -> ('a * 'b) output_tuple

type 'a non_empty_list = 'a * 'a list

type ('named, 'positional) parsed = {
  p_named : 'named output_tuple;
  p_pos : 'positional node list;
}

let parsed_name (description_name, description_convert) action =
  Smap.find_opt description_name action.named
  |> Option.map (fun (((_, loc) as x), _) -> (description_convert x, loc))

let rec all_keys : type a. a descr_tuple -> string list =
 fun names ->
  match names with
  | [] -> []
  | (action_key, _) :: rest -> action_key :: all_keys rest

let check_is_unused : type a. action -> a descr_tuple -> unit =
 fun action descriptions ->
  let all_keys = all_keys descriptions in
  Smap.iter
    (fun key (_, loc) ->
      if List.mem key all_keys then ()
      else
        let possible_arguments = all_keys in
        W.add
          (UnusedArgument
             {
               action_name = action.name;
               argument_name = key;
               loc;
               possible_arguments;
             }))
    action.named

let rec parsed_names : type a. action -> a descr_tuple -> a output_tuple =
 fun action descriptions ->
  match descriptions with
  | [] -> []
  | description :: rest ->
      let parsed =
        match parsed_name description action with
        | None -> None
        | Some (Error (`Msg msg), loc) ->
            W.add @@ Parsing_failure { msg; loc };
            None
        | Some (Ok a, _) -> Some a
      in
      parsed :: parsed_names action rest

let parse_atom ~named ~positional (action, loc) =
  let p_named = parsed_names action named in
  check_is_unused action named;
  let p_pos =
    List.map (fun (x, loc) -> (positional x, loc)) action.positional
  in
  ({ p_named; p_pos }, loc)

open W.M

let parse ~action_name ~named ~positional s :
    (('named, 'pos) parsed node non_empty_list * W.warnor list, _) result =
  let+ parsed_string = parse_string ~action_name s in
  let$ parsed_string = parsed_string in
  W.with_ @@ fun () ->
  List.map (parse_atom ~named ~positional) parsed_string |> function
  | [] ->
      assert false
      (* An empty string would be parsed as [ [[None; None; ...], []] ] *)
  | a :: rest -> ((a, rest) : _ non_empty_list)

let merge_positional (h, t) =
  List.concat_map
    (fun ({ p_named = ([] : _ output_tuple); p_pos = p }, _loc) -> p)
    (h :: t)

let require_single_action ~action_name x =
  match x with
  | ((_, loc) as a), rest ->
      let warnings =
        match (rest : _ list) with
        | [] -> ([] : _ list)
        | rest ->
            let msg =
              "Action " ^ action_name
              ^ " does not support ';'-separated arguments"
            in
            let loc = W.range loc rest in
            [ W.Parsing_failure { msg; loc } ]
      in
      (a, warnings)

let require_single_positional ~action_name (x : _ list) =
  W.with_ @@ fun () ->
  match x with
  | [] -> None
  | a :: rest ->
      let () =
        match rest with
        | [] -> ()
        | (_, loc) :: rest ->
            let msg =
              "Action " ^ action_name ^ " does not support multiple arguments"
            and loc = W.range loc rest in
            W.add (Parsing_failure { msg; loc })
      in
      Some a

let no_args ~action_name s =
  let open W.M in
  let+ x = parse ~action_name ~named:[] ~positional:id s in
  let$ x = x in
  match x with
  | ({ p_named = []; p_pos = [] }, _loc), [] -> ((), [])
  | (_, loc), _ ->
      let msg = "The " ^ action_name ^ " action does not accept any argument" in
      ((), [ W.Parsing_failure { msg; loc } ])

let parse_only_els ~action_name s =
  let+ x, warnings = parse ~action_name ~named:[] ~positional:id s in
  let res = match merge_positional x with [] -> `Self | x -> `Ids x in
  (res, warnings)

let parse_only_el ~action_name s =
  let+ x, warnings = parse ~action_name ~named:[] ~positional:id s in
  match merge_positional x with
  | [] -> (`Self, warnings)
  | x :: rest ->
      let warnings =
        match rest with
        | [] -> warnings
        | (_, loc) :: _ ->
            let msg = "Expected a single ID" in
            let w = W.Parsing_failure { msg; loc } in
            w :: warnings
      in
      (`Id x, warnings)

let option_to_error error = function
  | Some x -> Ok x
  | None -> Error (`Msg error)

let duration =
  ( "duration",
    fun (x, _) ->
      x |> Float.of_string_opt |> option_to_error "Error during float parsing"
  )

let margin =
  ( "margin",
    fun (x, _) ->
      x |> Float.of_string_opt |> option_to_error "Error during float parsing"
  )