package uniq

  1. Overview
  2. Docs
A library which introspect and infer dependencies via codept, ocamlfind and opam

Install

dune-project
 Dependency

Authors

Maintainers

Sources

uniq-0.1.1.tbz
sha256=d7781ec4fedda463465b6c6fe99ab377a1a8f9c34a658a17d29a9b2fe414b85c
sha512=0cdd5273918d2c24f3ae6cb837650ca25d72ae0f714fc8d10238ae8f01572ae6dc2771f952a22cc4fc6981fcccd57e222f766aebfc39b0e8599078d6839e8a1d

doc/src/uniq.solver/uniq_solver.ml.html

Source file uniq_solver.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
let src = Logs.Src.create "uniq.solver"

module Log = (val Logs.src_log src : Logs.LOG)
module MSet = Set.Make (Modname)
module Info = Uniq_info
module Digest = Uniq_digest

let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
let somef fmt = Fmt.kstr Stdlib.Option.some fmt

type cfg = {
    stdlib: bool
  ; recurse: bool
  ; exclude: Fpath.t list
  ; ignore: MSet.t
  ; forbid: MSet.t
}

type private_module = Modname.t * Uniq_digest.t option

let absolute =
  (* NOTE(dinosaure): [Fpath.v] should be fine! *)
  let cwd = Fpath.v (Sys.getcwd ()) in
  fun path ->
    let path = if Fpath.is_rel path then Fpath.(cwd // path) else path in
    Fpath.normalize path

let config ?(stdlib = true) ?(recurse = true) ?(exclude = []) ?(ignore = [])
    ?(forbid = []) () =
  let ignore = MSet.of_list ignore in
  let forbid = MSet.of_list forbid in
  let exclude = List.map absolute exclude in
  { stdlib; recurse; exclude; ignore; forbid }

let to_ignore ~cfg modname = MSet.mem modname cfg.ignore

type providers = ?crc:Digest.t -> Modname.t -> Info.t option
type disambiguate = Modname.t -> Info.t list -> Info.t

exception Multiple_solutions of Modname.t * Digest.t option * Info.t list

let () =
  let dummy = String.make (Digest.length * 2) '-' in
  Printexc.register_printer @@ function
  | Multiple_solutions (modname, crc, infos) ->
      somef "Multiple solutions for %a (%a): @[<hov>%a@]" Modname.pp modname
        Fmt.(option ~none:(const string dummy) Digest.pp)
        crc
        Fmt.(list ~sep:(any ";@ ") Info.pp)
        infos
  | _ -> None

(* NOTE(dinosaure): The purpose of this function is to reclassify
   dependencies based on what has just been injected. For example, adding
   [cmdliner.cmi] means that the [Cmd] module and the [Term] module can be
   resolved without requiring any further information. *)
let prune ?disambiguate infos modules =
  let fn0 (m, crc) (p, crc') =
    match (crc, crc') with
    | Some crc, Some crc' when Digest.equal crc crc' ->
        let part = Info.Path.singleton m in
        Info.Path.is_a_part ~part p
    | Some _, Some _ -> false
    | Some _, None -> false
    | None, Some _ | None, None ->
        let part = Info.Path.singleton m in
        Info.Path.is_a_part ~part p
  in
  let fn1 (m, crc) info =
    let exports = Info.exports info in
    let found = List.exists (fn0 (m, crc)) exports in
    if found then Some info else None
  in
  let take (infos, _, rem) crc m solution =
    Log.debug (fun mf -> mf "take %a for %a" Info.pp solution Modname.pp m);
    let location = Info.location solution in
    let fn info = Info.qualify info ~location ?crc `Intf m in
    (List.map fn infos, true, rem)
  in
  let fn0 (infos, progress, rem) (m, crc) =
    match List.filter_map (fn1 (m, crc)) infos with
    | [ solution ] -> take (infos, progress, rem) crc m solution
    | _ :: _ as solutions -> (
        (* The same module is exported by several collected interfaces (e.g.
           [Term] by both [cmdliner] and [mnotty]): defer to [disambiguate]
           when the caller provides one, otherwise report the ambiguity. *)
        match disambiguate with
        | Some choose -> take (infos, progress, rem) crc m (choose m solutions)
        | None -> raise (Multiple_solutions (m, crc, solutions)))
    | [] -> (infos, progress, (m, crc) :: rem)
  in
  let rec go infos modules =
    let infos, progress, modules =
      List.fold_left fn0 (infos, false, []) modules
    in
    if progress then go infos modules else (infos, modules)
  in
  go infos modules

let missing_intfs infos =
  List.concat_map (fun info -> fst (Info.missing info)) infos

let not_in_forbidden_modules cfg modules =
  match List.filter (fun (m, _) -> MSet.mem m cfg.forbid) modules with
  | [] -> Ok ()
  | forbidden ->
      error_msgf "@[<hov>the project requires forbidden module(s): %a@]"
        Fmt.(list ~sep:(any ",@ ") Modname.pp)
        (List.map fst forbidden)

let sort = List.sort_uniq (fun (a, _) (b, _) -> Modname.compare a b)

(* NOTE(dinosaure): a module [m] is a submodule of [solution] if [solution]
   exports it. *)
let exported_by (m, crc) solution =
  let part = Info.Path.singleton m in
  let fn (p, crc') =
    match (crc, crc') with
    | Some crc, Some crc' ->
        Digest.equal crc crc' && Info.Path.is_a_part ~part p
    | Some _, None -> false
    | None, _ -> Info.Path.is_a_part ~part p
  in
  List.exists fn (Info.exports solution)

let postpone ~providers modules =
  let fn (m, crc) = (m, crc, providers ?crc m) in
  let candidates = List.map fn modules in
  let fn (m, crc, r) = Option.map (fun r -> (m, crc, r)) r in
  let candidates = List.filter_map fn candidates in
  let fn (m, crc) =
    let fn (m', _, _) = Modname.compare m m' <> 0 in
    let others = List.filter fn candidates in
    not (List.exists (fun (_, _, s) -> exported_by (m, crc) s) others)
  in
  List.filter fn modules

let solve_intfs ?disambiguate ~cfg:({ recurse; exclude; stdlib; _ } as cfg)
    ~providers dirs =
  let ( let* ) = Result.bind in
  let fn = Uniq_resolve.Src.sources ~recurse ~exclude in
  let dirs = List.map absolute dirs in
  let dirs = List.map Fpath.to_dir_path dirs in
  let srcs = List.map fn dirs in
  let fn (infos, progress, rem) (m, crc) =
    begin match providers ?crc m with
    | None -> (infos, progress, (m, crc) :: rem)
    | Some solution when List.exists (Info.equal solution) infos = false ->
        let location = Info.location solution in
        (* NOTE(dinosaure): here, [crc] is still the one from what it miss
           but, with the solution, we can actually fix it with what [solution]
           gives to us. We can observe some requalification from [Location] to
           [Fully_qualified] which is fine but I suspect an override of the
           [crc] sometimes... *)
        let fn info = Info.qualify info ~location ?crc `Intf m in
        let infos = List.map fn infos in
        Log.debug (fun m -> m "add %a" Info.pp solution);
        (solution :: infos, true, rem)
    | Some _solution ->
        (* NOTE(dinosaure): The provider points at an artifact we already hold,
           yet the module is still unresolved (typically a digest mismatch).
           Leave it pending so it surfaces as a hole rather than aborting. *)
        (infos, progress, (m, crc) :: rem)
    end
  in
  let rec go infos =
    match missing_intfs infos |> sort with
    | [] -> Ok (infos, [])
    | modules ->
        let* () = not_in_forbidden_modules cfg modules in
        let infos, modules = prune ?disambiguate infos modules in
        let modules = sort modules in
        let infos, progress, modules =
          match modules with
          | [] -> (infos, true, [])
          | modules ->
              (* NOTE(dinosaure): Subtle but useful. Here, we try to "postpone"
                 certain solutions if their names are exported by a "top-level"
                 module. For example, we look for the [Path] module, which is
                 found as [path.cmi] ([compiler-libs]) but also as a submodule
                 of [colombe.cmi]. The idea is to try [colombe.cmi] first (which
                 also exports [Path]) and, if that doesn't work, to try
                 [path.cmi] next. It's a rather odd heuristic which, in a way,
                 prioritises resolving sub-modules first and then lets the CRC
                 help us; if there's still no solution, it then suggests the
                 top-level modules. Not sure what implications this might have,
                 though... *)
              begin match postpone ~providers modules with
              | [] -> List.fold_left fn (infos, false, []) modules
              | now ->
                  let fn1 (m, _) =
                    let fn2 (m', _) = Modname.compare m m' = 0 in
                    List.exists fn2 now
                  in
                  let later = List.filter (Fun.negate fn1) modules in
                  let infos, progress, rem =
                    List.fold_left fn (infos, false, []) now
                  in
                  if progress then (infos, progress, List.rev_append later rem)
                  else List.fold_left fn (infos, false, rem) later
              end
        in
        if progress then go infos else Ok (infos, modules)
  in
  let* infos = Uniq_resolve.qualify ~stdlib srcs in
  let* infos, modules =
    try go infos
    with Multiple_solutions (m, _crc, solutions) ->
      error_msgf "@[<v>%a is provided by several incompatible interfaces:@,%a@]"
        Modname.pp m
        Fmt.(list ~sep:cut (any "  " ++ Info.pp))
        solutions
  in
  (* NOTE(dinosaure): delete modules that we can ignore. *)
  let fn (m, _) = not (MSet.mem m cfg.ignore) in
  let modules = List.filter fn modules in
  Ok (infos, modules)