Skip to content

Commit 1fd630d

Browse files
committed
feat: implement per-module library dependency filtering (issue #4572)
Use ocamldep output to filter cross-library file dependencies per module. Instead of every module depending on all libraries, each module now depends only on libraries whose entry modules it actually references. - Lib_index maps library entry module names back to libraries, computed once per stanza and stored in Compilation_context - read_immediate_deps_raw_of returns raw (unresolved) module names from ocamldep, including cross-library references previously discarded - build_cm unions external references across transitive intra-stanza deps to handle transparent module aliases (module M = Mylib) - Filtering is disabled for stanzas whose requires include virtual library implementations, since their requires may not include the virtual library - deps_of_entries extended to support per-file deps for unwrapped libraries - Dep_graph.dir accessor added for link-time module detection - deps_with_exts removed (dead code after Includes refactor) Signed-off-by: Robin Bate Boerop <me@robinbb.com>
1 parent 8c3d290 commit 1fd630d

File tree

19 files changed

+327
-95
lines changed

19 files changed

+327
-95
lines changed

src/dune_rules/compilation_context.ml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ module Includes = struct
55
type t = Command.Args.without_targets Command.Args.t Lib_mode.Cm_kind.Map.t
66

77
(* Library file dependencies (Hidden_deps) are added per-module in
8-
module_compilation.ml rather than here.
9-
TODO: some of the requires can be filtered out using [ocamldep] info.
10-
See issue #4572. *)
8+
module_compilation.ml based on ocamldep output. Each module depends
9+
only on libraries it actually references.
10+
See issue #4572: Finer dependency analysis between libraries. *)
1111
let make ~project ~direct_requires ~hidden_requires lib_config
1212
: _ Lib_mode.Cm_kind.Map.t
1313
=
@@ -68,6 +68,7 @@ type t =
6868
; parameters : Module_name.t list Resolve.Memo.t
6969
; instances : Parameterised_instances.t Resolve.Memo.t option
7070
; includes : Includes.t
71+
; lib_index : Lib_file_deps.Lib_index.t Resolve.Memo.t
7172
; preprocessing : Pp_spec.t
7273
; opaque : bool
7374
; js_of_ocaml : Js_of_ocaml.In_context.t option Js_of_ocaml.Mode.Pair.t
@@ -95,6 +96,7 @@ let requires_hidden t = t.requires_hidden
9596
let requires_link t = Memo.Lazy.force t.requires_link
9697
let parameters t = t.parameters
9798
let includes t = t.includes
99+
let lib_index t = t.lib_index
98100
let preprocessing t = t.preprocessing
99101
let opaque t = t.opaque
100102
let js_of_ocaml t = t.js_of_ocaml
@@ -218,6 +220,11 @@ let create
218220
; implements
219221
; parameters
220222
; includes = Includes.make ~project ~direct_requires ~hidden_requires ocaml.lib_config
223+
; lib_index =
224+
(let open Resolve.Memo.O in
225+
let* direct_libs = direct_requires
226+
and* hidden_libs = hidden_requires in
227+
Lib_file_deps.Lib_index.create super_context (direct_libs @ hidden_libs) ~for_)
221228
; preprocessing
222229
; opaque
223230
; js_of_ocaml
@@ -318,6 +325,7 @@ let for_module_generated_at_link_time cctx ~requires ~module_ =
318325
; flags = Ocaml_flags.empty
319326
; requires_link = Memo.lazy_ (fun () -> requires)
320327
; requires_compile = requires
328+
; lib_index = Resolve.Memo.return Lib_file_deps.Lib_index.empty
321329
; includes
322330
; modules
323331
}

src/dune_rules/compilation_context.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ val requires_hidden : t -> Lib.t list Resolve.Memo.t
6262
val requires_compile : t -> Lib.t list Resolve.Memo.t
6363
val parameters : t -> Module_name.t list Resolve.Memo.t
6464
val includes : t -> Command.Args.without_targets Command.Args.t Lib_mode.Cm_kind.Map.t
65+
val lib_index : t -> Lib_file_deps.Lib_index.t Resolve.Memo.t
6566
val preprocessing : t -> Pp_spec.t
6667
val opaque : t -> bool
6768
val js_of_ocaml : t -> Js_of_ocaml.In_context.t option Js_of_ocaml.Mode.Pair.t

src/dune_rules/dep_graph.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type t =
77
}
88

99
let make ~dir ~per_module = { dir; per_module }
10+
let dir t = t.dir
1011

1112
let deps_of t (m : Module.t) =
1213
match Module_name.Unique.Map.find t.per_module (Module.obj_name m) with

src/dune_rules/dep_graph.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ val make
99
-> per_module:Module.t list Action_builder.t Module_name.Unique.Map.t
1010
-> t
1111

12+
val dir : t -> Path.Build.t
1213
val deps_of : t -> Module.t -> Module.t list Action_builder.t
1314
val top_closed_implementations : t -> Module.t list -> Module.t list Action_builder.t
1415

src/dune_rules/lib_file_deps.ml

Lines changed: 136 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,39 @@ let deps_of_lib (lib : Lib.t) ~groups =
4848
|> Dep.Set.of_list
4949
;;
5050

51+
let deps_of_module (lib : Lib.t) (m : Module.t) ~cm_kinds =
52+
let obj_dir = Lib.info lib |> Lib_info.obj_dir in
53+
List.filter_map cm_kinds ~f:(fun kind ->
54+
Obj_dir.Module.cm_public_file obj_dir m ~kind |> Option.map ~f:(fun p -> Dep.file p))
55+
|> Dep.Set.of_list
56+
;;
57+
5158
let deps libs ~groups = Dep.Set.union_map libs ~f:(deps_of_lib ~groups)
5259

53-
let deps_of_entries ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) (libs : Lib.t list) =
54-
let groups =
60+
let deps_of_entries ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) entries =
61+
let groups, cm_kinds =
5562
match cm_kind with
56-
| Ocaml Cmi | Ocaml Cmo -> [ Group.Ocaml Cmi ]
57-
| Melange Cmi -> [ Group.Melange Cmi ]
58-
| Melange Cmj -> [ Group.Melange Cmi; Melange Cmj ]
59-
| Ocaml Cmx -> [ Group.Ocaml Cmi; Ocaml Cmx ]
63+
| Ocaml Cmi | Ocaml Cmo -> [ Group.Ocaml Cmi ], [ Lib_mode.Cm_kind.Ocaml Cmi ]
64+
| Melange Cmi -> [ Group.Melange Cmi ], [ Melange Cmi ]
65+
| Melange Cmj -> [ Group.Melange Cmi; Melange Cmj ], [ Melange Cmi; Melange Cmj ]
66+
| Ocaml Cmx -> [ Group.Ocaml Cmi; Ocaml Cmx ], [ Ocaml Cmi; Ocaml Cmx ]
6067
in
61-
Dep.Set.union_map libs ~f:(fun lib ->
62-
let groups =
68+
List.map entries ~f:(fun ((lib : Lib.t), mod_opt) ->
69+
let is_opaque_local =
6370
match cm_kind with
64-
| Ocaml Cmx when opaque && Lib.is_local lib -> [ Group.Ocaml Cmi ]
65-
| _ -> groups
71+
| Ocaml Cmx -> opaque && Lib.is_local lib
72+
| _ -> false
6673
in
67-
deps_of_lib lib ~groups)
74+
match mod_opt with
75+
| Some m ->
76+
let cm_kinds =
77+
if is_opaque_local then [ Lib_mode.Cm_kind.Ocaml Cmi ] else cm_kinds
78+
in
79+
deps_of_module lib m ~cm_kinds
80+
| None ->
81+
let groups = if is_opaque_local then [ Group.Ocaml Cmi ] else groups in
82+
deps_of_lib lib ~groups)
83+
|> List.fold_left ~init:Dep.Set.empty ~f:Dep.Set.union
6884
;;
6985

7086
type path_specification =
@@ -98,3 +114,112 @@ let eval ~loc ~expander ~paths:path_spec (deps : Dep_conf.t list) =
98114
| Some _ -> raise_disallowed_external_path ~loc lib_name path));
99115
paths
100116
;;
117+
118+
module Lib_index = struct
119+
(** Each entry pairs a library with an optional Module.t: None means use
120+
glob deps (wrapped libs, external unwrapped), Some m means use
121+
per-file deps via Obj_dir.Module.cm_file (local unwrapped). *)
122+
type entry = Lib.t * Module.t option
123+
124+
type t =
125+
{ by_module_name : entry list Module_name.Map.t
126+
; unresolved : Lib.t list
127+
}
128+
129+
let module_names_of_lib sctx (lib : Lib.t) ~for_
130+
: (Module_name.t * Module.t option) list option Resolve.Memo.t
131+
=
132+
let open Resolve.Memo.O in
133+
let* main_module = Lib.main_module_name lib in
134+
match main_module with
135+
| Some name ->
136+
(* Wrapped library: index the wrapper name plus all inner module
137+
names so that references via -open flags are resolved. All map
138+
to None (glob deps) since the wrapper exposes the whole lib. *)
139+
let info = Lib.info lib in
140+
(match Lib_info.entry_modules info ~for_ with
141+
| Lib_info.Source.External _ ->
142+
Resolve.Memo.return (Some [ name, None ])
143+
| Lib_info.Source.Local ->
144+
Resolve.Memo.lift_memo
145+
(let open Memo.O in
146+
let+ modules_opt = Dir_contents.modules_of_lib sctx lib ~for_ in
147+
match modules_opt with
148+
| None -> Some [ name, None ]
149+
| Some modules_with_vlib ->
150+
let modules = Modules.With_vlib.drop_vlib modules_with_vlib in
151+
let inner_names =
152+
Modules.fold modules ~init:[] ~f:(fun m acc ->
153+
(Module.name m, None) :: acc)
154+
in
155+
Some ((name, None) :: inner_names)))
156+
| None ->
157+
let info = Lib.info lib in
158+
(match Lib_info.entry_modules info ~for_ with
159+
| Lib_info.Source.External (Ok names) ->
160+
Resolve.Memo.return (Some (List.map names ~f:(fun n -> n, None)))
161+
| Lib_info.Source.External (Error _) -> Resolve.Memo.return None
162+
| Lib_info.Source.Local ->
163+
Resolve.Memo.lift_memo
164+
(let open Memo.O in
165+
let+ modules_opt = Dir_contents.modules_of_lib sctx lib ~for_ in
166+
match modules_opt with
167+
| None -> None
168+
| Some modules_with_vlib ->
169+
let modules = Modules.With_vlib.drop_vlib modules_with_vlib in
170+
let entry_modules = Modules.entry_modules modules in
171+
Some (List.map entry_modules ~f:(fun m -> Module.name m, Some m))))
172+
;;
173+
174+
let empty = { by_module_name = Module_name.Map.empty; unresolved = [] }
175+
176+
let create sctx (libs : Lib.t list) ~for_ : t Resolve.Memo.t =
177+
let open Resolve.Memo.O in
178+
let+ entries =
179+
Resolve.Memo.List.map libs ~f:(fun lib ->
180+
let+ names_opt = module_names_of_lib sctx lib ~for_ in
181+
lib, names_opt)
182+
in
183+
let by_module_name, unresolved =
184+
List.fold_left
185+
entries
186+
~init:(Module_name.Map.empty, [])
187+
~f:(fun (by_name, unresolved) (lib, names_opt) ->
188+
match names_opt with
189+
| None -> by_name, lib :: unresolved
190+
| Some named_modules ->
191+
let by_name =
192+
List.fold_left named_modules ~init:by_name ~f:(fun acc (name, mod_opt) ->
193+
Module_name.Map.update acc name ~f:(function
194+
| None -> Some [ lib, mod_opt ]
195+
| Some entries -> Some ((lib, mod_opt) :: entries)))
196+
in
197+
by_name, unresolved)
198+
in
199+
{ by_module_name; unresolved }
200+
;;
201+
202+
let filter_libs (index : t) ~(referenced_modules : Module_name.Set.t) : entry list =
203+
let from_refs =
204+
Module_name.Set.fold referenced_modules ~init:[] ~f:(fun name acc ->
205+
match Module_name.Map.find index.by_module_name name with
206+
| None -> acc
207+
| Some entries -> List.rev_append entries acc)
208+
in
209+
let unresolved = List.map index.unresolved ~f:(fun lib -> lib, None) in
210+
(* Entries are unique per (lib, module name). Two modules with the
211+
same name can't appear for the same library since module names are
212+
unique within a library's entry modules. *)
213+
let compare (a_lib, a_mod) (b_lib, b_mod) =
214+
match Lib.compare a_lib b_lib with
215+
| (Lt | Gt) as c -> c
216+
| Eq ->
217+
(match a_mod, b_mod with
218+
| None, None -> Eq
219+
| None, Some _ -> Lt
220+
| Some _, None -> Gt
221+
| Some a, Some b -> Module_name.compare (Module.name a) (Module.name b))
222+
in
223+
List.rev_append unresolved from_refs |> List.sort_uniq ~compare
224+
;;
225+
end

src/dune_rules/lib_file_deps.mli

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ end
1515
with extension [files] of libraries [libs]. *)
1616
val deps : Lib.t list -> groups:Group.t list -> Dep.Set.t
1717

18-
(** Compute library file dependencies for all [libs] for the given [cm_kind].
19-
When [opaque] is true, local libraries only depend on .cmi (not .cmx). *)
20-
val deps_of_entries : opaque:bool -> cm_kind:Lib_mode.Cm_kind.t -> Lib.t list -> Dep.Set.t
21-
2218
type path_specification =
2319
| Allow_all
2420
| Disallow_external of Lib_name.t
@@ -31,3 +27,27 @@ val eval
3127
-> paths:path_specification
3228
-> Dep_conf.t list
3329
-> Path.Set.t Memo.t
30+
31+
module Lib_index : sig
32+
type entry = Lib.t * Module.t option
33+
type t
34+
35+
val empty : t
36+
37+
val create
38+
: Super_context.t
39+
-> Lib.t list
40+
-> for_:Compilation_mode.t
41+
-> t Resolve.Memo.t
42+
43+
val filter_libs : t -> referenced_modules:Module_name.Set.t -> entry list
44+
end
45+
46+
(** Compute library file dependencies for the given entries and cm_kind.
47+
Entries with [Some module_] use per-file deps; [None] uses glob deps.
48+
When [opaque] is true, local libraries only depend on .cmi (not .cmx). *)
49+
val deps_of_entries
50+
: opaque:bool
51+
-> cm_kind:Lib_mode.Cm_kind.t
52+
-> Lib_index.entry list
53+
-> Dep.Set.t

0 commit comments

Comments
 (0)