Source file project.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
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
include Visitor
module StringSet = Set.Make (String)
module StringMap = Util.StringMap
module ModSet = Util.IntSet
module ModMap = Util.IntMap
module ModGraph = Graph.Make (Int)
module ModId = struct
type t = int
let to_int mod_id = mod_id
end
type mod_id = ModId.t
let global_scope = -1
type l = Lexing.position * Lexing.position
let to_loc l = Parse_ast.Range (fst l, snd l)
type 'a spanned = 'a * l
type selector = S_tree | S_only
type value = V_string of string | V_bool of bool | V_selector of selector * string | V_list of value list
let string_value s = V_string s
let bool_value b = V_bool b
let parse_assignment ~variables s =
match String.index_from_opt s 0 '=' with
| None -> false
| Some n ->
let var = String.trim (String.sub s 0 n) in
let arg =
match String.trim (String.sub s (n + 1) (String.length s - (n + 1))) with
| "true" -> V_bool true
| "false" -> V_bool false
| s -> V_string s
in
variables := StringMap.add var arg !variables;
true
type exp =
| E_app of string * exp spanned list
| E_file of string * string
| E_id of string
| E_if of exp spanned * exp spanned * exp spanned
| E_list of exp spanned list
| E_op of exp spanned * string * exp spanned
| E_parent
| E_string of string
| E_value of value
| E_var of string
type 'a non_empty = 'a * 'a list
type dependency =
| D_requires of exp spanned non_empty
| D_after of exp spanned non_empty
| D_before of exp spanned non_empty
type mdl_def = M_dep of dependency | M_directory of exp spanned | M_module of mdl | M_files of exp spanned non_empty
and mdl = { name : string spanned; defs : mdl_def spanned list; span : l }
type def = Def_root of string | Def_var of string spanned * exp spanned | Def_module of mdl | Def_test of string list
let mk_root root = (Def_root root, (Lexing.dummy_pos, Lexing.dummy_pos))
class type project_visitor = object
method vexp : exp spanned -> exp spanned visit_action
method vdependency : l -> dependency -> dependency visit_action
method vmodule : mdl -> mdl visit_action
method vdef : def spanned -> def spanned visit_action
method on_root_change : string -> unit
method short_circuit_if : bool
end
let rec visit_exp vis outer_exp =
let aux vis no_change =
match no_change with
| E_file _, _ | E_string _, _ | E_id _, _ | E_var _, _ | E_parent, _ | E_value _, _ -> no_change
| E_if (i, t, e), l ->
let i' = visit_exp vis i in
begin
match i' with
| E_value (V_bool b), _ when vis#short_circuit_if -> if b then visit_exp vis t else visit_exp vis e
| _ ->
let t' = visit_exp vis t in
let e' = visit_exp vis e in
if i == i' && t == t' && e == e' then no_change else (E_if (i', t', e'), l)
end
| E_list xs, l ->
let xs' = map_no_copy (visit_exp vis) xs in
if xs == xs' then no_change else (E_list xs', l)
| E_op (lhs, op, rhs), l ->
let lhs' = visit_exp vis lhs in
let rhs' = visit_exp vis rhs in
if lhs == lhs' && rhs == rhs' then no_change else (E_op (lhs', op, rhs'), l)
| E_app (f, xs), l ->
let xs' = map_no_copy (visit_exp vis) xs in
if xs == xs' then no_change else (E_app (f, xs'), l)
in
do_visit vis (vis#vexp outer_exp) aux outer_exp
let visit_dependency vis l outer_dependency =
let aux vis no_change =
match no_change with
| D_requires (e, es) ->
let e' = visit_exp vis e in
let es' = map_no_copy (visit_exp vis) es in
if e == e' && es == es' then no_change else D_requires (e', es')
| D_after (e, es) ->
let e' = visit_exp vis e in
let es' = map_no_copy (visit_exp vis) es in
if e == e' && es == es' then no_change else D_after (e', es')
| D_before (e, es) ->
let e' = visit_exp vis e in
let es' = map_no_copy (visit_exp vis) es in
if e == e' && es == es' then no_change else D_before (e', es')
in
do_visit vis (vis#vdependency l outer_dependency) aux outer_dependency
let rec visit_module vis (outer_module : mdl) =
let visit_module_def vis no_change =
match no_change with
| M_dep dep, l ->
let dep' = visit_dependency vis l dep in
if dep == dep' then no_change else (M_dep dep', l)
| M_directory e, l ->
let e' = visit_exp vis e in
if e == e' then no_change else (M_directory e', l)
| M_module m, l ->
let m' = visit_module vis m in
if m == m' then no_change else (M_module m', l)
| M_files (e, es), l ->
let e' = visit_exp vis e in
let es' = map_no_copy (visit_exp vis) es in
if e == e' && es == es' then no_change else (M_files (e', es'), l)
in
let aux vis (({ name; defs; span } : mdl) as no_change) =
let defs' = map_no_copy (visit_module_def vis) defs in
if defs == defs' then no_change else { name; defs = defs'; span }
in
do_visit vis (vis#vmodule outer_module) aux outer_module
let visit_def vis outer_def =
let aux vis no_change =
match no_change with
| Def_test _, _ -> no_change
| Def_var (v, exp), l ->
let exp' = visit_exp vis exp in
if exp == exp' then no_change else (Def_var (v, exp'), l)
| Def_module m, l ->
let m' = visit_module vis m in
if m == m' then no_change else (Def_module m', l)
| Def_root root, _ ->
vis#on_root_change root;
no_change
in
do_visit vis (vis#vdef outer_def) aux outer_def
let visit_defs vis defs = map_no_copy (visit_def vis) defs
let rec value_to_strings l = function
| V_string s -> [(s, l)]
| V_list vs -> List.concat (List.map (value_to_strings l) vs)
| _ -> raise (Reporting.err_typ (to_loc l) "Expected strings")
let rec to_strings = function
| (E_value v, l) :: xs -> value_to_strings l v @ to_strings xs
| (_, l) :: _ -> raise (Reporting.err_typ (to_loc l) "String has not been evaluated")
| [] -> []
let rec value_to_selectors l = function
| V_selector (sel, s) -> [((sel, s), l)]
| V_string s -> [((S_tree, s), l)]
| V_list vs -> List.concat (List.map (value_to_selectors l) vs)
| _ -> raise (Reporting.err_typ (to_loc l) "Expected module selector")
let rec to_selectors = function
| (E_value v, l) :: xs -> value_to_selectors l v @ to_selectors xs
| (_, l) :: _ -> raise (Reporting.err_typ (to_loc l) "Module selector has not been evaluated")
| [] -> []
class empty_project_visitor : project_visitor =
object
method vexp _ = DoChildren
method vdependency _ _ = DoChildren
method vmodule _ = DoChildren
method vdef _ = DoChildren
method on_root_change _ = ()
method short_circuit_if = false
end
let project_binop l op lhs rhs =
let invalid_arguments () = raise (Reporting.err_typ (to_loc l) ("Invalid arguments for '" ^ op ^ "'")) in
match op with
| "==" -> (
match (lhs, rhs) with
| V_string s1, V_string s2 -> V_bool (s1 = s2)
| V_bool b1, V_bool b2 -> V_bool (b1 = b2)
| _, _ -> invalid_arguments ()
)
| "!=" -> (
match (lhs, rhs) with
| V_string s1, V_string s2 -> V_bool (s1 <> s2)
| V_bool b1, V_bool b2 -> V_bool (b1 <> b2)
| _, _ -> invalid_arguments ()
)
| "/" -> (
match (lhs, rhs) with
| V_string lhs, V_string rhs -> V_string (lhs ^ Filename.dir_sep ^ rhs)
| _, _ -> invalid_arguments ()
)
| _ -> raise (Reporting.err_typ (to_loc l) ("Unknown binary operator '" ^ op ^ "'"))
let project_app l f xs =
match f with
| "only" -> (
match xs with
| [V_string mod_name] -> V_selector (S_only, mod_name)
| _ -> raise (Reporting.err_typ (to_loc l) "Invalid argument for only")
)
| "tree" -> (
match xs with
| [V_string mod_name] -> V_selector (S_tree, mod_name)
| _ -> raise (Reporting.err_typ (to_loc l) "Invalid argument for tree")
)
| "error" -> (
match xs with
| [V_string msg] -> raise (Reporting.err_general (to_loc l) ("Error: " ^ msg))
| _ -> raise (Reporting.err_typ (to_loc l) "Invalid arguments for error")
)
| _ -> raise (Reporting.err_typ (to_loc l) ("Unknown function '" ^ f ^ "'"))
class eval_visitor (vars : value StringMap.t ref) =
object
inherit empty_project_visitor
val mutable declared : StringSet.t = StringSet.empty
method! vdef def =
let aux no_change =
match no_change with
| Def_var ((name, l), (E_value v, _)), _ ->
if StringSet.mem name declared then
raise (Reporting.err_typ (to_loc l) ("Variable " ^ name ^ " has already been declared"));
if not (StringMap.mem name !vars) then vars := StringMap.add name v !vars;
declared <- StringSet.add name declared;
no_change
| Def_var (_, (_, l)), _ -> Reporting.unreachable (to_loc l) __POS__ "Value has not been evaluated"
| _ -> no_change
in
ChangeDoChildrenPost (def, aux)
method! vexp outer_exp =
let aux no_change =
match no_change with
| (E_string s | E_id s), l -> (E_value (V_string s), l)
| E_file (f, ext), l -> (E_value (V_string (f ^ "." ^ ext)), l)
| E_parent, l -> (E_value (V_string Filename.parent_dir_name), l)
| E_var var, l -> begin
match StringMap.find_opt var !vars with
| Some v -> (E_value v, l)
| None -> raise (Reporting.err_typ (to_loc l) ("Could not find variable " ^ var))
end
| E_op ((E_value lhs, _), op, (E_value rhs, _)), l -> (E_value (project_binop l op lhs rhs), l)
| E_app (f, xs), l ->
let xs =
List.map
(function
| E_value v, _ -> v
| _, l -> Reporting.unreachable (to_loc l) __POS__ "Argument has not been fully evaluated"
)
xs
in
(E_value (project_app l f xs), l)
| E_list xs, l ->
let xs =
List.map
(function
| E_value v, _ -> v
| _, l -> Reporting.unreachable (to_loc l) __POS__ "Value in list has not been fully evaluated"
)
xs
in
(E_value (V_list xs), l)
| E_value _, _ -> no_change
| E_if ((_, l), _, _), _ -> raise (Reporting.err_typ (to_loc l) "Expected boolean value in if")
| _, l -> Reporting.unreachable (to_loc l) __POS__ "Value has not been fully evaluated"
in
ChangeDoChildrenPost (outer_exp, aux)
method! short_circuit_if = true
end
type project_structure = {
names : string spanned array;
ids : int StringMap.t;
mutable parents : int ModMap.t;
mutable children : ModGraph.graph;
mutable files : string spanned list ModMap.t;
mutable requires : ModGraph.graph;
mutable deps : ModGraph.graph;
}
class order_visitor (xs : string spanned list ref) =
object
inherit empty_project_visitor
method! vexp _ = SkipChildren
method! vmodule m =
xs := m.name :: !xs;
DoChildren
end
let add_child parent child map =
ModMap.update parent
(function None -> Some (ModSet.singleton child) | Some children -> Some (ModSet.add child children))
map
let get_parents id proj =
let parents = ref ModSet.empty in
let rec loop child =
match ModMap.find_opt child proj.parents with
| Some parent ->
parents := ModSet.add parent !parents;
loop parent
| None -> ()
in
loop id;
!parents
let link_parent id parents proj =
match parents with
| parent :: _ ->
proj.children <- add_child parent id proj.children;
proj.parents <- ModMap.add id parent proj.parents;
proj.deps <- ModGraph.add_edge id parent proj.deps
| [] -> ()
let rec collect_files = function
| (M_files (f, fs), _) :: mdefs -> to_strings (f :: fs) @ collect_files mdefs
| _ :: mdefs -> collect_files mdefs
| [] -> []
let add_root root_opt (file, l) =
match root_opt with Some root -> (root ^ Filename.dir_sep ^ file, l) | None -> (file, l)
class structure_visitor (proj : project_structure) =
object
inherit empty_project_visitor
val mutable parents : int list = []
val mutable last_root : string option = None
method! vexp _ = SkipChildren
method! vdependency _ _ = SkipChildren
method! vmodule m =
let name = fst m.name in
let id = StringMap.find name proj.ids in
let files = collect_files m.defs in
proj.files <- ModMap.add id (List.map (add_root last_root) files) proj.files;
link_parent id parents proj;
parents <- id :: parents;
ChangeDoChildrenPost
( m,
fun m ->
parents <- List.tl parents;
m
)
method! on_root_change new_root = last_root <- Some new_root
end
type frame = {
directory : string option;
requires : (selector * string) spanned list;
after : (selector * string) spanned list;
before : (selector * string) spanned list;
}
let empty_frame = { directory = None; requires = []; after = []; before = [] }
type stack = frame list
let get_from_frame f stack proj =
let rec go acc = function
| frame :: frames ->
go
(List.fold_left
(fun acc ((selector, name), l) ->
match (selector, StringMap.find_opt name proj.ids) with
| S_only, Some id -> ModSet.add id acc
| S_tree, Some id ->
ModSet.union acc (ModGraph.reachable (ModSet.singleton id) ModSet.empty proj.children)
| _, None -> raise (Reporting.err_general (to_loc l) ("Module " ^ name ^ " does not exist"))
)
acc (f frame)
)
frames
| [] -> acc
in
go ModSet.empty stack
let get_requires = get_from_frame (fun frame -> frame.requires)
let get_after = get_from_frame (fun frame -> frame.after)
let get_before = get_from_frame (fun frame -> frame.before)
let update_head f = function x :: xs -> f x :: xs | [] -> []
class dependency_visitor (proj : project_structure) =
object
inherit empty_project_visitor
val mutable stack : stack = []
method! vexp _ = SkipChildren
method! vdependency _ dep =
begin
match dep with
| D_requires (e, es) ->
stack <- update_head (fun frame -> { frame with requires = frame.requires @ to_selectors (e :: es) }) stack
| D_before (e, es) ->
stack <- update_head (fun frame -> { frame with before = frame.before @ to_selectors (e :: es) }) stack
| D_after (e, es) ->
stack <- update_head (fun frame -> { frame with after = frame.after @ to_selectors (e :: es) }) stack
end;
SkipChildren
method! vmodule m =
let name = fst m.name in
let id = StringMap.find name proj.ids in
stack <- empty_frame :: stack;
ChangeDoChildrenPost
( m,
fun m ->
let requires = get_requires stack proj in
let before = get_before stack proj in
let after = get_after stack proj in
proj.requires <- ModMap.add id (ModSet.union (get_parents id proj) requires) proj.requires;
proj.deps <- ModGraph.add_edges id [] proj.deps;
proj.deps <- ModSet.fold (fun r -> ModGraph.add_edge id r) requires proj.deps;
proj.deps <- ModSet.fold (fun b -> ModGraph.add_edge b id) before proj.deps;
proj.deps <- ModSet.fold (fun a -> ModGraph.add_edge id a) after proj.deps;
stack <- List.tl stack;
m
)
end
let run_tests defs (proj : project_structure) =
let run_test_cmd l cmd args =
let invalid_cmd () = raise (Reporting.err_general (to_loc l) ("Invalid test command " ^ cmd)) in
match cmd with
| "dep_graph" ->
let chan =
match List.nth_opt args 0 with Some "stderr" -> stderr | Some "stdout" -> stdout | _ -> invalid_cmd ()
in
let reduce_req = match List.nth_opt args 1 with Some "reduce_req" -> true | _ -> false in
let darken id color = match ModMap.find_opt id proj.files with Some [] -> color | _ -> color ^ "3" in
ModGraph.make_multi_dot
~node_color:(fun id -> darken id "chartreuse")
~edge_color:(fun _ _ -> "black")
~string_of_node:(fun id -> fst proj.names.(id))
chan
[
("dotted", ModGraph.reverse (ModGraph.transitive_reduction proj.deps));
( "solid",
ModGraph.reverse (if reduce_req then ModGraph.transitive_reduction proj.requires else proj.requires)
);
]
| _ -> ()
in
List.iter (function Def_test (cmd :: args), l -> run_test_cmd l cmd args | _ -> ()) defs
let initialize_project_structure ~variables defs =
let xs = ref [] in
let _ = visit_defs (new order_visitor xs) defs in
let names = Array.of_list (List.rev !xs) in
let ids =
snd (Array.fold_left (fun (n, m) name -> (n + 1, StringMap.add (fst name) n m)) (0, StringMap.empty) names)
in
let defs = visit_defs (new eval_visitor variables) defs in
let proj =
{
names;
ids;
parents = ModMap.empty;
children = ModMap.empty;
files = ModMap.empty;
requires = ModMap.empty;
deps = ModGraph.empty;
}
in
let _ = visit_defs (new structure_visitor proj) defs in
let _ = visit_defs (new dependency_visitor proj) defs in
run_tests defs proj;
proj
let rec shorten_scc loop g =
match loop with
| x :: xs -> begin
match Util.find_next (fun y -> ModGraph.has_edge x y g) (List.rev xs) with
| tail, Some (y, _) -> x :: shorten_scc (y :: List.rev tail) g
| tail, None -> x :: shorten_scc (List.rev tail) g
end
| [] -> []
let get_module_id proj name = StringMap.find_opt name proj.ids
let get_children id proj = ModGraph.reachable (ModSet.singleton id) ModSet.empty proj.children
let required_modules ~roots (proj : project_structure) =
let reqs = ModGraph.prune roots ModSet.empty proj.requires in
fun id -> ModMap.mem id reqs
let valid_module_id proj id = 0 <= id && id < Array.length proj.names
let module_name proj id = proj.names.(id)
let module_order proj =
let original_order = List.init (Array.length proj.names) (fun x -> x) in
let sccs : int list list = ModGraph.scc ~original_order proj.deps in
let rec flatten = function
| [id] :: components -> id :: flatten components
| [] -> []
| (id :: ids) :: _ ->
let name, l = proj.names.(id) in
let loop = shorten_scc (id :: ids) proj.deps in
let loop = Util.string_of_list " -> " (fun id -> fst proj.names.(id)) loop ^ " -> " ^ fst proj.names.(id) in
raise
(Reporting.err_general (to_loc l)
("Cyclic modules detected: " ^ name ^ " depends on itself\n\nCycle: " ^ loop)
)
| [] :: _ -> Reporting.unreachable Parse_ast.Unknown __POS__ "Empty component"
in
flatten sccs
let module_files proj id = ModMap.find id proj.files
let module_requires (proj : project_structure) id = ModMap.find id proj.requires |> ModSet.elements
let all_files proj = List.map (fun id -> ModMap.find id proj.files) (module_order proj) |> List.concat
let all_modules proj = List.map snd (StringMap.bindings proj.ids)