summaryrefslogtreecommitdiff
path: root/src/config_diff.ml
blob: 2458901bc888cd3d503f8c6e8c938affc4f30d4a (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
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
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
type change = Unchanged | Added | Subtracted | Updated of string list

exception Incommensurable
exception Empty_comparison
exception Nonexistent_child

module Diff_tree = struct
    type t = { left: Config_tree.t;
               right: Config_tree.t;
               add: Config_tree.t;
               sub: Config_tree.t;
               del: Config_tree.t;
               inter: Config_tree.t;
             }
end

module Diff_compare = struct
    type t = { left: Config_tree.t;
               right: Config_tree.t;
               skel: Config_tree.t;
               ppath: string list;
               udiff: string;
             }
end

module Diff_show = struct
    type t = { left: Config_tree.t;
               right: Config_tree.t;
               base_path: string list;
               open_blocks: string list list;
               config_diff: string;
             }
end

type _ diff_result =
    | Diff_tree : Diff_tree.t -> Diff_tree.t diff_result
    | Diff_compare : Diff_compare.t -> Diff_compare.t diff_result
    | Diff_show : Diff_show.t -> Diff_show.t diff_result

let eval_diff_result : type a. a diff_result -> a = function
    | Diff_tree x -> x
    | Diff_compare x -> x
    | Diff_show x -> x

type 'a diff_func = ?recurse:bool -> string list -> 'a diff_result -> change -> 'a diff_result

let make_diff_trees l r = Diff_tree { left = l; right = r;
                                add = (Config_tree.make "");
                                sub = (Config_tree.make "");
                                del = (Config_tree.make "");
                                inter = (Config_tree.make "");
}

let make_diff_compare l r = Diff_compare { left = l; right = r;
                                skel = (Config_tree.make "");
                                ppath = [];
                                udiff = "";
                           }

let make_diff_show l r path = Diff_show { left = l; right = r;
                                base_path = path;
                                open_blocks = [];
                                config_diff = "";
                              }

let name_of n = Vytree.name_of_node n
let data_of n = Vytree.data_of_node n
let children_of n = Vytree.children_of_node n
let make data name children = Vytree.make_full data name children

module ValueOrd = struct
    type t = string
    let compare a b =
        Util.lexical_numeric_compare a b
end
module ValueS = Set.Make(ValueOrd)

module TreeOrd = struct
    type t = Config_tree.t
    let compare a b =
        Util.lexical_numeric_compare (name_of a) (name_of b)
end
module ChildrenS = Set.Make(TreeOrd)

(* unordered set of values *)
module ValueSet = Set.Make(String)

let (^~) (node : Config_tree.t) (node' : Config_tree.t) =
  name_of node = name_of node' &&
  (data_of node).values <> (data_of node').values

let left_opt_pairs n m =
    (children_of n) |> List.map (fun x ->
        let maybe_node =
            (children_of m) |> List.find_opt (fun y ->
                name_of y = name_of x) in
        (Some x, maybe_node))

let right_opt_pairs n m =
    (children_of m) |> List.map (fun y ->
        let maybe_node =
            (children_of n) |> List.find_opt (fun x ->
                name_of x = name_of y) in
        (maybe_node, Some y))

let opt_tuple_cmp t1 t2 =
    let opt_tuple_val t =
        match t with
        | None, None -> ""
        | Some x, None -> name_of x
        | None, Some y -> name_of y
        | Some x, Some _ -> name_of x
    in Util.lexical_numeric_compare (opt_tuple_val t1) (opt_tuple_val t2)

let opt_zip n m =
    left_opt_pairs n m @ right_opt_pairs n m |> List.sort_uniq opt_tuple_cmp

let get_opt_name left_opt right_opt =
    match left_opt, right_opt with
    | Some left_node, None -> name_of left_node
    | None, Some right_node -> name_of right_node
    | Some left_node, Some _ -> name_of left_node
    | None, None -> raise Empty_comparison

let update_path path left_opt right_opt =
    let name = get_opt_name left_opt right_opt in
    if name = "" then path
    else path @ [name]

(* tree diff algorithm: walk the tree pair, calling a function of type
   diff_func for each comparison.
   The idea of matching on pairs of (node opt) is from
   https://github.com/LukeBurgessYeo/tree-diff
 *)
let rec diff (path : string list) (f : 'a diff_func) (res: 'a diff_result) ((left_node_opt, right_node_opt) : Config_tree.t option * Config_tree.t option) =
    let path = update_path path left_node_opt right_node_opt in
    match left_node_opt, right_node_opt with
    | None, None -> raise Empty_comparison
    | Some _, None -> f path res Subtracted
    | None, Some _ -> f path res Added
    | Some left_node, Some right_node when left_node = right_node ->
        f ~recurse:true path res Unchanged
    | Some left_node, Some right_node when left_node ^~ right_node ->
        let values = (data_of right_node).values in
        f path res (Updated values)
    | Some left_node, Some right_node ->
        let ret = f ~recurse:false path res Unchanged in
        List.fold_left (diff path f) ret (opt_zip left_node right_node)

(* copy node paths between trees *)
let rec clone_path ?(recurse=true) ?(set_values=None) old_root new_root path_done path_remaining =
    (* raises:
        [Vytree.Nonexistent_path]
       alert exn Vytree.get:
        [Vytree.Empty_path] not possible as clone_path called by clone with non-empty path
        [Vytree.Nonexistent_path] allow raise
       alert exn Vytree.insert:
        [Vytree.Empty_path] not possible as clone_path called by clone with non-empty path
        [Not_found] not possible for postion=Lexical
        [Vytree.Duplicate_child] not possible as calls are on path complement
        [Vytree.Insert_error] not possible as calls are on path_existing @ [name]
     *)
    match path_remaining with
    | [] | [_] ->
        let path_total = path_done @ path_remaining in
        let old_node = (Vytree.get[@alert "-exn"]) old_root path_total in
        let data =
            match set_values with
            | Some v -> { (data_of old_node) with Config_tree.values = v }
            | None -> data_of old_node
        in
        if recurse then
            (Vytree.insert[@alert "-exn"]) ~position:Lexical ~children:(children_of old_node) new_root path_total data
        else
            (Vytree.insert[@alert "-exn"]) ~position:Lexical new_root path_total data
    | name :: names ->
        let path_done = path_done @ [name] in
        let old_node = (Vytree.get[@alert "-exn"]) old_root path_done in
        let new_root = (Vytree.insert[@alert "-exn"]) ~position:Lexical new_root path_done (data_of old_node) in
        clone_path ~recurse:recurse ~set_values:set_values old_root new_root path_done names

let clone ?(recurse=true) ?(set_values=None) old_root new_root path =
    (* raises:
        [Vytree.Nonexistent_path] from clone_path
     *)
    match path with
    | [] -> if recurse then old_root else new_root
    | _ ->
            let path_existing = Vytree.get_existent_path new_root path in
            let path_remaining = Vylist.complement path path_existing in
            clone_path ~recurse:recurse ~set_values:set_values old_root new_root path_existing path_remaining

(* define the diff_func *)
let build_trees ?(recurse=true) (path : string list) (Diff_tree res) (m : change) =
    (* raises no exception:
        clone will always be called on extant path of left or right
       alert exn Vytree.get_values:
        [Vytree.Empty_path] not possible as pattern Updated implies non-empty path
        [Vytree.Nonexistent_path] not possible as pattern Updated implies path exists
     *)
    match m with
    | Added -> Diff_tree {res with add = clone res.right res.add path; }
    | Subtracted ->
        Diff_tree {res with sub = clone res.left res.sub path;
         del = clone ~recurse:false ~set_values:(Some []) res.left res.del path; }
    | Unchanged ->
        Diff_tree {res with inter = clone ~recurse:recurse res.left res.inter path; }
    | Updated v ->
            (* if in this case, node at path is guaranteed to exist *)
            let ov = (Config_tree.get_values[@alert "-exn"]) res.left path in
            match ov, v with
            | [_], [_] -> Diff_tree {res with sub = clone res.left res.sub path;
                           del = clone res.left res.del path;
                           add = clone res.right res.add path; }
            | _, _ -> let ov_set = ValueS.of_list ov in
                      let v_set = ValueS.of_list v in
                      let sub_vals = ValueS.elements (ValueS.diff ov_set v_set) in
                      let add_vals = ValueS.elements (ValueS.diff v_set ov_set) in
                      let inter_vals = ValueS.elements (ValueS.inter ov_set v_set) in
                      let sub_tree =
                          if not (Util.is_empty sub_vals) then
                              clone ~set_values:(Some sub_vals) res.left res.sub path
                          else
                              res.sub
                      in
                      let del_tree =
                          if not (Util.is_empty sub_vals) then
                              if (Util.is_empty add_vals) && (Util.is_empty inter_vals) then
                                  (* delete whole node, not just values *)
                                  clone ~set_values:(Some []) res.left res.del path
                              else
                                  clone ~set_values:(Some sub_vals) res.left res.del path
                          else
                              res.del
                      in
                      let add_tree =
                          if not (Util.is_empty add_vals) then
                            clone ~set_values:(Some add_vals) res.right res.add path
                          else
                              res.add
                      in
                      let inter_tree =
                          if not (Util.is_empty inter_vals) then
                              clone ~set_values:(Some inter_vals) res.left res.inter path
                          else
                              res.inter
                      in Diff_tree { res with add = add_tree;
                           sub = sub_tree;
                           del = del_tree;
                           inter = inter_tree; }

(* get sub trees for path-relative comparison *)
let tree_at_path path node =
    (* raises:
        [Vytree.Empty_path]
        [Empty_comparison]
       alert exn Vytree.get:
        [Vytree.Empty_path] allow raise
        [Vytree.Nonexistent_path] catch and raise Empty_comparison
     *)
    try
        let node = (Vytree.get[@alert "-exn"]) node path in
        make Config_tree.default_data "" [node]
    with Vytree.Nonexistent_path -> raise Empty_comparison

(* call recursive diff on config_trees with build_trees as the diff_func *)
let diff_trees path left right =
    (* raises:
        [Empty_comparison] from tree_at_path
        [Incommensurable]
     *)
    if (name_of left) <> (name_of right) then
        raise Incommensurable
    else
        let (left, right) = if not (path = []) then
            (tree_at_path path left, tree_at_path path right) else (left, right) in
        let trees = make_diff_trees left right in
        let d = diff [] build_trees trees (Option.some left, Option.some right)
        in eval_diff_result d

(* wrapper to return single tree with diff trees as subtrees *)
let diff_tree path left right =
    (* raises:
        [Incommensurable],
        [Empty_comparison] from compare
     *)
    let trees = diff_trees path left right in
    let add_node = make Config_tree.default_data "add" (children_of (trees.add)) in
    let sub_node = make Config_tree.default_data "sub" (children_of (trees.sub)) in
    let del_node = make Config_tree.default_data "del" (children_of (trees.del)) in
    let int_node = make Config_tree.default_data "inter" (children_of (trees.inter)) in
    let ret = make Config_tree.default_data "" [add_node; sub_node; del_node; int_node] in
    ret

(* convenience function needed for commit algorithm:
    we need a hybrid tree between the 'del' tree and the 'sub' tree, namely:
    in case the del tree has a terminal tag node (== all tag values have
    been removed) add tag node values for proper removal in commit execution
 *)

let get_tagged_delete_tree dt =
    (* alert exn Config_tree.is_tag:
        [Vytree.Empty_path] not possible in pattern non-empty path
        [Vytree.Nonexistent_path] not possible in fold_tree_with_path
       alert exn Vytree.is_terminal_path:
        [Vytree.Empty_path] not possible in pattern non-empty path
       alert exn Vytree.children_of_path:
        [Vytree.Empty_path] not possible in pattern non-empty path
        [Vytree.Nonexistent_path] not possible in super-tree of fold_tree_with_path arg
       alert exn Vytree.insert:
        [Vytree.Empty_path]: not possible since called on pattern path non-empty
        [Not_found]: not possible for postion=Lexical
        [Vytree.Duplicate_child]: not possible by condition is_terminal_path
        [Vytree.Insert_error]: not possible since constructed iteratively from existing path
     *)
    let del_tree = Config_tree.get_subtree dt ["del"] in
    let sub_tree = Config_tree.get_subtree dt ["sub"] in
    let f (p, a) _t =
        let q = List.rev p in
        match q with
        | [] -> (p, a)
        | _ ->
        if (Config_tree.is_tag[@alert "-exn"]) a q && (Vytree.is_terminal_path[@alert "-exn"]) a q then
            let children = (Vytree.children_of_path[@alert "-exn"]) sub_tree q in
            let insert_child path node name =
                (Vytree.insert[@alert "-exn"]) ~position:Lexical node (path @ [name]) Config_tree.default_data
            in
            let a' = List.fold_left (insert_child q) a children in
            (p, a')
        else
            (p, a)
    in
    Vytree.fold_tree_with_path f ([], del_tree) del_tree


(* the following builds a diff_func to return a unified diff string of
   configs or config commands for use in the config-mode 'compare' command
 *)

let list_but_last l =
    let len = List.length l in
    List.filteri (fun i _ -> i < len - 1) l

let path_to_string (path: string list) =
    Printf.sprintf "[%s]\n" (String.concat " " path)

let marked_render mark node =
    let lines = Config_tree.render_config node in
    let l = String.split_on_char '\n' lines in
    let m =
        List.map (fun s -> if (String.length s) > 0 then mark ^ s else s) l in
    String.concat "\n" m

let added_lines ?(cmds=false) node path =
    (* alert exn Config_tree.render_commands:
        [Vytree.Nonexistent_path] not possible on root path
     *)
    if not cmds then marked_render "+ " (tree_at_path path node)
    else
        ((Config_tree.render_commands[@alert "-exn"]) ~op:Set node []) ^ "\n"

let removed_lines ?(cmds=false) node path =
    (* alert exn Config_tree.render_commands:
        [Vytree.Nonexistent_path] not possible on root path
     *)
    if not cmds then marked_render "- " (tree_at_path path node)
    else
        ((Config_tree.render_commands[@alert "-exn"]) ~op:Delete node []) ^ "\n"

let order_commands (strl: string) =
    let l = String.split_on_char '\n' strl in
    let del = List.filter (fun s -> (s <> "") && (s.[0] = 'd')) l in
    let set = List.filter (fun s -> (s <> "") && (s.[0] = 's')) l in
    (String.concat "\n" del) ^ "\n" ^ (String.concat "\n" set) ^ "\n"

let unified_diff ?(cmds=false) ?recurse:_ (path : string list) (Diff_compare res) (m : change) =
    (* raises no exception:
        clone will always be called on extant path of left or right
       alert exn Vytree.get_values:
        [Vytree.Empty_path] not possible as pattern Updated implies non-empty path
        [Vytree.Nonexistent_path] not possible as pattern Updated implies path exists
     *)
    let ppath_l = list_but_last path
    in
    let ppath_s =
        if (ppath_l <> res.ppath) then path_to_string ppath_l
        else ""
    in
    let str_diff =
        if not cmds then res.udiff ^ ppath_s
        else res.udiff
    in
    match m with
    | Added ->
            let str_diff =
                let add_tree = clone res.right res.skel path in
                str_diff ^ (added_lines ~cmds:cmds add_tree path)
            in
            Diff_compare { res with ppath = ppath_l; udiff = str_diff; }
    | Subtracted ->
            let str_diff =
                let sub_tree = clone res.left res.skel path in
                str_diff ^ (removed_lines ~cmds:cmds sub_tree path)
            in
            Diff_compare { res with ppath = ppath_l; udiff = str_diff; }
    | Unchanged -> Diff_compare (res)
    | Updated v ->
            let ov = (Config_tree.get_values[@alert "-exn"]) res.left path in
            match ov, v with
            | [_], [_] ->
                    let str_diff =
                        let sub_tree = clone res.left res.skel path in
                        str_diff ^ (removed_lines ~cmds:cmds sub_tree path)
                    in
                    let str_diff =
                        let add_tree = clone res.right res.skel path in
                        str_diff ^ (added_lines ~cmds:cmds add_tree path)
                    in
                    Diff_compare { res with ppath = ppath_l; udiff = str_diff; }
            | _, _ -> let ov_set = ValueS.of_list ov in
                      let v_set = ValueS.of_list v in
                      let sub_vals = ValueS.elements (ValueS.diff ov_set v_set) in
                      let add_vals = ValueS.elements (ValueS.diff v_set ov_set) in
                      let str_diff =
                          if not (Util.is_empty sub_vals) then
                              let sub_tree =
                                  clone ~set_values:(Some sub_vals) res.left res.skel path
                              in str_diff ^ (removed_lines ~cmds:cmds sub_tree path)
                          else str_diff
                      in
                      let str_diff =
                          if not (Util.is_empty add_vals) then
                              let add_tree =
                                  clone ~set_values:(Some add_vals) res.right res.skel path
                              in str_diff ^ (added_lines ~cmds:cmds add_tree path)
                          else str_diff
                      in
                      Diff_compare { res with ppath = ppath_l; udiff = str_diff; }

let add_empty_path src_node dest_node path =
    clone ~recurse:false ~set_values:(Some []) src_node dest_node path

let compare_at_path_maybe_empty left right path =
    let left =
        try
            tree_at_path path left
        with Empty_comparison ->
            try
                let left = add_empty_path right left path in
                tree_at_path path left
             with Vytree.Nonexistent_path ->
                 raise Empty_comparison
     and right =
        try
            tree_at_path path right
        with Empty_comparison ->
            try
                let right = add_empty_path left right path in
                tree_at_path path right
             with Vytree.Nonexistent_path ->
                 raise Empty_comparison
    in (left, right)

let diff_compare ?(cmds=false) path left right =
    (* raises:
        [Incommensurable],
        [Empty_comparison] from compare_at_path_maybe_empty
     *)
    if (name_of left) <> (name_of right) then
        raise Incommensurable
    else
        let (left, right) =
            if (path <> []) then
                compare_at_path_maybe_empty left right path
            else (left, right) in
        let dstr = make_diff_compare left right in
        let dstr =
            diff [] (unified_diff ~cmds:cmds) dstr (Option.some left, Option.some right)
        in
        let dstr = eval_diff_result dstr in
        let strs =
            if cmds then order_commands dstr.udiff
            else dstr.udiff
        in
        strs

(* the following builds a diff_func for 'show config' *)

let annotate_rendered change rendered =
    let mark =
        match change with
        | Unchanged -> " "
        | Added -> "+"
        | Subtracted -> "-"
        | Updated _ -> ">"
    in
    let lst = String.split_on_char '\n' rendered in
    let marked = List.map (fun x -> match x with "" -> x | _ -> mark ^ x) lst in
    String.concat "\n" marked

let get_level_at_path node path =
    (* alert exn Config_tree.is_tag_value:
        [Vytree.Empty_path] called in pattern path non-empty
        [Vytree.Nonexistent_path] function diff never calls diff_func on nonexistent path
     *)
    let f level p =
        if (Config_tree.is_tag_value[@alert "-exn"]) node p then level
        else level + 1
    in
    match path with
    | [] -> 0
    | _ ->
        List.fold_left f 0 (Util.flag path) - 1

let render_level_open indent node path =
    (* alert exn Config_tree.is_tag, Config_tree.is_tag_value:
        [Vytree.Empty_path] called in branch path non-empty
        [Vytree.Nonexistent_path] function diff never calls diff_func on nonexistent path
     *)
    if Util.is_empty path || (Config_tree.is_tag[@alert "-exn"]) node path then
        ""
    else
    let level = get_level_at_path node path in
    let indent_str = Config_tree.make_indent indent level in
    if (Config_tree.is_tag_value[@alert "-exn"]) node path then
        let tag_node =
            match Util.get_last_n path 1 with
            | None -> (* not possible as path non-empty *) "none"
            | Some n -> n
        in
        let tag_value =
            match Util.get_last path with
            | None -> (* not possible as path non-empty *) "none"
            | Some v -> v
        in
        Printf.sprintf "%s%s %s {\n" indent_str tag_node tag_value
    else
        let name =
            match Util.get_last path with
            | None -> (* not possible as path non-empty *) "none"
            | Some v -> v
        in
        Printf.sprintf "%s%s {\n" indent_str name

let render_level_close indent node path =
    (* alert exn Config_tree.is_tag:
        [Vytree.Empty_path] called in branch path non-empty
        [Vytree.Nonexistent_path] function diff never calls diff_func on nonexistent path
     *)
    if Util.is_empty path || (Config_tree.is_tag[@alert "-exn"]) node path then
        ""
    else
    let level = get_level_at_path node path in
    let indent_str = Config_tree.make_indent indent level in
    Printf.sprintf "%s}\n" indent_str

let config_diff (rt : Reference_tree.t) ?(recurse=true) (path : string list) (Diff_show res) (m : change) =
    (* alert exn Vytree.get, Reference_tree.refpath, Config_tree.get_values,
       Reference_tree.is_multi, Config_tree.is_tag_value:
        [Vytree.Empty_path] checked at only point possible (Unchanged)
        [Vytree.Nonexistent_path] function diff never calls diff_func on nonexistent path
     *)

    let indent = 4 in
    (* the only subtlety in all this is the bookkeeping of closing open
       braces at correct level:
       (1) a rendered line with open brace will occur before the next
       depth-first step
       (2) at each return to (local) root, the path is checked for the
       matching closing brace
       explicitly: the record field open_blocks is a list of open paths
       ordered by reverse inclusion, which are closed when the path being
       passed to config_diff no longer contains that element
     *)
    let rec close_blocks s l =
        match l with
        | [] -> s, []
        | h :: tl ->
            if Util.is_sublist h path then
                s, l
            else
                let rendered = render_level_close indent res.left h in
                let s' = s ^ annotate_rendered Unchanged rendered
                in close_blocks s' tl
    in
    let diff_str, rev_blocks = close_blocks res.config_diff res.open_blocks
    in
    match m with
    | Added ->
        let node = (Vytree.get[@alert "-exn"]) res.right path in
        let level = get_level_at_path res.right path in
        let rendered =
            Config_tree.render_node indent level node
        in
        let rev_diff = diff_str ^ annotate_rendered m rendered in
        Diff_show {res with config_diff = rev_diff; open_blocks = rev_blocks;}
    | Subtracted ->
        let node = (Vytree.get[@alert "-exn"]) res.left path in
        let level = get_level_at_path res.left path in
        let rendered =
            Config_tree.render_node indent level node
        in
        let rev_diff = diff_str ^ annotate_rendered m rendered in
        Diff_show {res with config_diff = rev_diff; open_blocks = rev_blocks;}
    | Unchanged ->
        begin
        match recurse with
        | false ->
            let rendered = render_level_open indent res.left path in
            let rev_diff = diff_str ^ annotate_rendered m rendered in
            Diff_show {res with config_diff = rev_diff; open_blocks = path::rev_blocks}
        | true ->
            match path with
            | [] -> (* case left = right *)
                let rendered =
                    Config_tree.render_config res.left
                in
                let rev_diff = diff_str ^ annotate_rendered m rendered in
                Diff_show {res with config_diff = rev_diff; open_blocks = rev_blocks;}
            | _ ->
                let level = get_level_at_path res.left path in
                let node = (Vytree.get[@alert "-exn"]) res.left path in
                let rendered =
                    Config_tree.render_node indent level node
                in
                let rev_diff = diff_str ^ annotate_rendered m rendered in
                Diff_show {res with config_diff = rev_diff; open_blocks = rev_blocks;}
        end
    | Updated v ->
        let refp =
            (Reference_tree.refpath[@alert "-exn"]) rt (res.base_path @ path) in
        let multi = (Reference_tree.is_multi[@alert "-exn"]) rt refp in
        let level = get_level_at_path res.left path in
        let indent_str =
            Config_tree.make_indent indent level
        in
        let name =
            match Util.get_last path with
            | None -> (* not possible *) "none"
            | Some n -> n
        in
        match multi with
        | false ->
            let rendered =
                Config_tree.render_values indent_str name v
            in
            let rev_diff = diff_str ^ annotate_rendered m rendered in
            Diff_show {res with config_diff = rev_diff; open_blocks = rev_blocks;}
        | true ->
            let ov = (Config_tree.get_values[@alert "-exn"]) res.left path in
            let ov_set = ValueS.of_list ov in
            let v_set = ValueS.of_list v in
            let sub_vals = ValueS.elements (ValueS.diff ov_set v_set) in
            let add_vals = ValueS.elements (ValueS.diff v_set ov_set) in
            let inter_vals = ValueS.elements (ValueS.inter ov_set v_set) in
            let sub_rendered =
                match sub_vals with
                | [] -> ""
                | _ ->
                    Config_tree.render_values indent_str name sub_vals
            in
            let sub_diff = annotate_rendered Subtracted sub_rendered in
            let add_rendered =
                match add_vals with
                | [] -> ""
                | _ ->
                    Config_tree.render_values indent_str name add_vals
            in
            let add_diff = annotate_rendered Added add_rendered in
            let inter_rendered =
                match inter_vals with
                | [] -> ""
                | _ ->
                    Config_tree.render_values indent_str name inter_vals
            in
            let inter_diff = annotate_rendered Unchanged inter_rendered in
            let value_diff = sub_diff ^ inter_diff ^ add_diff in
            let rev_diff = diff_str ^ value_diff in
            Diff_show {res with config_diff = rev_diff; open_blocks = rev_blocks;}

(* call recursive diff on config_trees with config_diff as the diff_func *)
let diff_show rt path left right =
    (* raises:
        [Incommensurable]
        [Empty_comparison]
     *)
    if (name_of left) <> (name_of right) then
        raise Incommensurable
    else
        let (left, right) =
            if not (Util.is_empty path) then
            let with_node =
            match Reference_tree.get_path_type rt path with
            | `Leaf -> true
            | _ -> false
            in
            (Config_tree.get_subtree ~with_node left path,
            Config_tree.get_subtree ~with_node right path)
            else (left, right)
        in
        let config_show = make_diff_show left right path in
        let ret = diff [] (config_diff rt) config_show (Option.some left, Option.some right) in
        (* close final braces *)
        let d = config_diff rt ~recurse:false [] ret Unchanged in
        let diff_show_result = eval_diff_result d in
        diff_show_result.config_diff

(* mask function; mask applied on right *)
let mask_func_inclusive ?recurse:_ (path : string list) (Diff_tree res) (m : change) =
    (* alert exn Vytree.delete:
        [Vytree.Empty_path] not possible since Unchanged pattern is only empty path
        [Vytree.Nonexistent_path] not possible as called on existing config paths (res.left)
       alert exn Vytree.is_terminal_path:
        [Vytree.Empty_path] not possible since Unchanged pattern is only empty path
     *)
    match m with
    | Added -> Diff_tree (res)
    | Subtracted ->
        begin
            match path with
            | [_] ->
                Diff_tree {res with left = (Vytree.delete[@alert "-exn"]) res.left path}
            |  _  ->
                if not ((Vytree.is_terminal_path[@alert "-exn"]) res.right (list_but_last path)) then
                    Diff_tree {res with left = (Vytree.delete[@alert "-exn"]) res.left path}
                else Diff_tree (res)
        end
    | Unchanged -> Diff_tree (res)
    | Updated _ -> Diff_tree (res)

(* mask function; mask applied on right *)
let mask_func_exclusive ?(recurse=true) (path : string list) (Diff_tree res) (m : change) =
    (* alert exn Vytree.delete:
        [Vytree.Empty_path] not possible in pattern match case
        [Vytree.Nonexistent_path] not possible as called on existing config paths (res.left)
       alert exn Vytree.is_terminal_path:
        [Vytree.Empty_path] not possible in pattern match case
     *)
    match m with
    | Added -> Diff_tree (res)
    | Subtracted -> Diff_tree (res)
    | Unchanged | Updated _ ->
        begin
            match path with
            | [] ->
                if recurse then
                    (* in the diff function, recurse = true on an empty path means that the
                       trees are equal, hence exclude all: return default (empty) tree *)
                    Diff_tree {res with left = Config_tree.default}
                else Diff_tree(res)
            | _ ->
                if recurse || ((Vytree.is_terminal_path[@alert "-exn"]) res.right path) then
                    let tmp = (Vytree.delete[@alert "-exn"]) res.left path in
                    let left' = (Config_tree.prune_delete[@alert "-exn"]) tmp path in
                    Diff_tree {res with left = left'}
                else Diff_tree (res)
        end

(* call recursive diff with mask_func; mask applied on right *)
let mask_tree ?(exclusive=false) left right =
    (* raises:
        [Empty_comparison] from diff
        [Incommensurable]
     *)
    if (name_of left) <> (name_of right) then
        raise Incommensurable
    else
    let trees = make_diff_trees left right in
    let mask_func =
        if exclusive then mask_func_exclusive else mask_func_inclusive
    in
    let d = diff [] mask_func trees (Option.some left, Option.some right)
    in
    let res = eval_diff_result d in
    res.left

(* Convert from a path that may or may not include intervening tag_values,
   returning a subtree of matches *)
exception Malformed_path of string

let subtree_from_partial reftree ctree result path =
    if Util.is_empty path then result
    else
    let check_reftree p =
        match p with
        | [] -> false
        | _ ->
            let rpath =
                Reference_tree.refpath_from_partial reftree p
            in
            match rpath with
            | [] -> false
            | _ -> true
    in
    let check_ctree p =
        match p with
        | [] -> false
        | _ -> (Vytree.exists[@alert "-exn"]) ctree p
    in
    let clone_node ?(recurse=false) tree p =
        if (Vytree.exists[@alert "-exn"]) tree p then
            tree
        else
        if not ((Vytree.exists[@alert "-exn"]) ctree p) then
            tree
        else
            clone ~recurse:recurse ctree tree p
    in
    let clone_children tree p =
        let children = Vytree.list_children ((Vytree.get[@alert "-exn"]) ctree p) in
        let paths = List.map (fun n -> p @ [n]) children in
        List.fold_left (clone_node ~recurse:true) tree paths
    in
    let rec aux acc path_done p =
        if not (check_reftree (path_done @ p)) then
            raise (Malformed_path (Util.string_of_list (path_done @ p)))
        else
        match path_done, p with
        | [], h :: tl ->
                if check_reftree [h] then aux (clone_node acc [h]) [h] tl
                else
                raise (Malformed_path (Util.string_of_list p))
        | _, h :: tl ->
                let p' = path_done @ [h] in
                if check_ctree p' then aux (clone_node acc p') p' tl
                else
                    if (Config_tree.is_tag[@alert "-exn"]) ctree path_done then
                    let children =
                        Vytree.list_children ((Vytree.get[@alert "-exn"]) ctree path_done)
                    in
                    let func accum child =
                        let path = path_done @ [child] @ [h] in
                        if check_ctree path then
                            aux (clone_node accum path) path tl
                        else accum
                    in
                    List.fold_left func acc children
                else
                (* [h] is a tag_value not present in the config tree *)
                raise (Malformed_path (Util.string_of_list p'))
        | _, [] -> clone_children acc path_done
    in aux result [] path


let union_of_values (n : Config_tree.t) (m : Config_tree.t) =
    let set_n = ValueS.of_list (data_of n).values in
    let set_m = ValueS.of_list (data_of m).values in
    ValueS.elements (ValueS.union set_n set_m)

let tree_union s t =
    (* raises:
        [Tree_alg.Incompatible_union]
        [Tree_alg.Nonexistent_child] should not be reachable
       alert exn Tree_alg.ConfigAlg.tree_union:
        [Tree_alg.Incompatible_union] allow raise
        [Tree_alg.Nonexistent_child] allow raise; should not be reachable
     *)
    let f u v =
        let values = union_of_values u v in
        let data = {(data_of v) with Config_tree.values = values} in
        Vytree.make_full data (name_of v) (children_of v)
    in
    (Tree_alg.ConfigAlg.tree_union[@alert "-exn"]) s t f

let tree_merge ?(destructive=false) s t =
    (* raises:
        [Tree_alg.Incompatible_union]
        [Tree_alg.Nonexistent_child] should not be reachable
       alert exn Tree_alg.ConfigAlg.tree_union:
        [Tree_alg.Incompatible_union] allow raise
        [Tree_alg.Nonexistent_child] allow raise; should not be reachable
     *)
    let f u v =
        let data =
            match destructive with
            | false -> data_of u
            | true  -> data_of v
        in Vytree.make_full data (name_of v) (children_of v)
    in
    (Tree_alg.ConfigAlg.tree_union[@alert "-exn"]) s t f