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)
| _ ->
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 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
| 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"
)