package uniq

  1. Overview
  2. Docs

Source file uniq_clos.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
module Info = Uniq_info
module Meta = Uniq_meta
module MSet = Set.Make (Modname)

module Archive = struct
  type t = Meta.archive

  let compare = Stdlib.compare
end

module ASet = Set.Make (Archive)

type env = {
    stdlib: Fpath.t option
  ; predicates: string list
  ; gamma: Info.t Fpath.Map.t
  ; roots: Fpath.t list
  ; pkgs: Meta.package list
}

type disambiguate = Modname.t -> Meta.Path.t list -> Meta.Path.t
type intf = Modname.t * Info.t
type impl = Modname.t * Info.t

let search_cmis ~roots =
  let elements path =
    if Sys.is_directory (Fpath.to_string path) then Ok false
    else if Fpath.mem_ext [ ".cmi" ] path then Ok true
    else Ok false
  in
  let traverse path =
    let fn root = Fpath.is_rooted ~root path || Fpath.equal root path in
    let traverse = List.exists fn roots in
    Ok traverse
  in
  let fn path acc =
    match Uniq_info.v path with
    | Ok info when Uniq_info.is_a_cmi info -> Fpath.Map.add path info acc
    | Ok _ | Error _ -> acc
  in
  let err _path _ = Ok () in
  Bos.OS.Path.fold ~err ~dotfiles:false ~elements:(`Sat elements)
    ~traverse:(`Sat traverse) fn Fpath.Map.empty roots

let env ?cfg roots =
  let ( let* ) = Result.bind in
  let stdlib =
    match cfg with
    | Some cfg -> Uniq_cfg.get cfg ~key:"standard_library" Uniq_cfg.Value.path
    | None -> None
  in
  let native =
    match cfg with
    | Some (where, _) ->
        let base = Fpath.basename (where :> Fpath.t) in
        not (String.length base >= 6 && String.sub base 0 6 = "ocamlc")
    | None -> true
  in
  let predicates = if native then [ "native" ] else [ "byte" ] in
  let* gamma = search_cmis ~roots in
  let pkgs = Meta.packages_with_archive roots in
  Ok { stdlib; predicates; gamma; roots; pkgs }

let gamma { gamma; _ } = gamma
let stdlib { stdlib; _ } = stdlib

let close_impls ~resolve intfs impls =
  let ( let* ) = Result.bind in
  let key info = Fpath.to_string (Info.location info) in
  let provided infos =
    let add_unit set (path, _) =
      match Info.Path.to_list path with [ m ] -> MSet.add m set | _ -> set
    in
    let add_info set info = List.fold_left add_unit set (Info.exports info) in
    List.fold_left add_info MSet.empty infos
  in
  let imported impls =
    let tbl = Hashtbl.create 0x7ff in
    let fn info =
      let impl (m, _) =
        if not (Hashtbl.mem tbl m) then Hashtbl.add tbl m (m, None)
      in
      let intf (m, crc) = Hashtbl.replace tbl m (m, crc) in
      List.iter impl (Info.impls_imported info);
      List.iter intf (Info.intfs_imported info)
    in
    List.iter fn impls;
    Hashtbl.fold (fun _ mc acc -> mc :: acc) tbl []
  in
  let seen = Hashtbl.create 0x7ff in
  List.iter (fun info -> Hashtbl.replace seen (key info) ()) impls;
  let rec go attempted impls =
    let known = provided (List.rev_append intfs impls) in
    let missing =
      imported impls
      |> List.filter (fun (m, _) ->
          (not (MSet.mem m known)) && not (MSet.mem m attempted))
    in
    match missing with
    | [] -> Ok impls
    | missing ->
        let fn acc (m, crc) =
          let* attempted, added = acc in
          let attempted = MSet.add m attempted in
          let* archives = resolve (m, crc) in
          let archives =
            List.filter (fun info -> not (Hashtbl.mem seen (key info))) archives
          in
          List.iter (fun info -> Hashtbl.replace seen (key info) ()) archives;
          Ok (attempted, List.rev_append archives added)
        in
        let* attempted, added =
          List.fold_left fn (Ok (attempted, [])) missing
        in
        if added = [] then Ok impls
        else go attempted (List.rev_append added impls)
  in
  go MSet.empty impls

let their_are_copies = function
  | [] -> true
  | witness :: rem ->
      let e = witness.Info.exports in
      let rem = List.map (fun info -> info.Info.exports) rem in
      let fn0 (m, crc) (m', crc') =
        match (crc, crc') with
        | Some crc, Some crc' ->
            Digest.equal crc crc' && Modname.compare m m' = 0
        | _, _ -> false
      in
      let fn1 e' = try List.for_all2 fn0 e e' with _ -> false in
      List.for_all fn1 rem

let prefer_stdlib ?stdlib solutions =
  match stdlib with
  | None -> List.hd solutions
  | Some dir ->
      let dir = Fpath.(normalize (to_dir_path dir)) in
      let in_stdlib info =
        let where = Info.location info in
        Fpath.equal dir Fpath.(normalize (to_dir_path (parent where)))
      in
      List.find_opt in_stdlib solutions
      |> Stdlib.Option.value ~default:(List.hd solutions)

let resolve ~env:{ stdlib; predicates; gamma; roots; pkgs } ~disambiguate
    (modname, crc) =
  let ( let* ) = Result.bind in
  let matches_crcs crc crc' =
    match (crc, crc') with
    | Some a, Some b -> Digest.equal a b
    | Some _, None | None, Some _ | None, None -> true
  in
  let matches info =
    let fn (m, crc') = Modname.compare modname m = 0 && matches_crcs crc crc' in
    List.exists fn info.Info.exports
  in
  let fn _ info acc = if matches info then info :: acc else acc in
  let candidates = Fpath.Map.fold fn gamma [] in
  let cmi =
    match candidates with
    | [] -> None
    | [ info ] -> Some info
    | _ :: _ as all when their_are_copies all ->
        Some (prefer_stdlib ?stdlib all)
    | _ :: _ ->
        Logs.warn (fun m ->
            m "Ambiguous implementation dependency %a; skipping" Modname.pp
              modname);
        None
  in
  match cmi with
  | None -> Ok []
  | Some cmi -> begin
      let* archive =
        Meta.from_cmi_to_impl ~roots ~packages:pkgs ?stdlib ~disambiguate
          (Info.location cmi)
      in
      match archive with
      | None -> Ok []
      | Some archive -> Meta.archives_of ~roots ~predicates archive
    end

let impls_from_intfs ~env:{ stdlib; roots; pkgs; predicates; _ } ~disambiguate
    intfs =
  let ( let* ) = Result.bind in
  let* descrs =
    let fn acc info =
      let* acc = acc in
      let* pkg =
        Meta.from_cmi_to_impl ~roots ~packages:pkgs ?stdlib ~disambiguate
          (Uniq_info.location info)
      in
      Ok ((info, pkg) :: acc)
    in
    List.fold_left fn (Ok []) intfs
  in
  let archives =
    let fn acc (_, pkg) =
      let none = acc and some a = ASet.add a acc in
      Stdlib.Option.fold ~none ~some pkg
    in
    List.fold_left fn ASet.empty descrs |> ASet.elements
  in
  let fn acc archive =
    let* acc = acc in
    let* infos = Meta.archives_of ~roots ~predicates archive in
    Ok (List.rev_append infos acc)
  in
  List.fold_left fn (Ok []) archives

let impls ~env ~disambiguate infos =
  let ( let* ) = Result.bind in
  let intfs = List.filter Info.is_a_cmi infos in
  let* impls = impls_from_intfs ~env ~disambiguate intfs in
  let resolve = resolve ~env ~disambiguate in
  close_impls ~resolve intfs impls

let verify ~env ~disambiguate infos =
  let ( let* ) = Result.bind in
  let intfs = List.filter Info.is_a_cmi infos in
  let* impls = impls_from_intfs ~env ~disambiguate intfs in
  let resolve = resolve ~env ~disambiguate in
  let* impls = close_impls ~resolve intfs impls in
  let sources = List.filter (Fun.negate Info.is_a_cmi) infos in
  let names infos =
    let add_unit set (path, _) =
      match List.rev (Info.Path.to_list path) with
      | leaf :: _ -> MSet.add leaf set
      | [] -> set
    in
    let add_info set info = List.fold_left add_unit set (Info.exports info) in
    List.fold_left add_info MSet.empty infos
  in
  let intf_provided = names (List.concat [ sources; intfs; impls ]) in
  let impl_provided = names (List.rev_append sources impls) in
  let holes imported_of provided nodes =
    let tbl = Hashtbl.create 0x7ff in
    let fn info =
      let fn (m, _) =
        if (not (MSet.mem m provided)) && not (Hashtbl.mem tbl m) then
          Hashtbl.add tbl m (m, info)
      in
      List.iter fn (imported_of info)
    in
    List.iter fn nodes;
    Hashtbl.fold (fun _ hole acc -> hole :: acc) tbl []
  in
  let intf_holes =
    holes Info.intfs_imported intf_provided
      (List.concat [ sources; intfs; impls ])
  in
  let impl_holes =
    holes Info.impls_imported impl_provided (List.rev_append sources impls)
  in
  Ok (intf_holes, impl_holes)