feat: finer dependency analysis between libraries (#4572)#14021
feat: finer dependency analysis between libraries (#4572)#14021robinbb wants to merge 10 commits intoocaml:mainfrom
Conversation
13c8e53 to
618ea7f
Compare
|
Thanks for working on this. I expect users are going to be quite pleased to have this feature. Some preliminary questions:
Why are the include flags are needed if we don't expect the library to be used by the module?
Do we have tests for when an internal module name is shadowing a library?
Is it possible to move these changes to a separate PR and rebase this work on top of that? |
7373af6 to
1892da3
Compare
@rgrinberg I have tried to do so with this PR #14030, depending on what you really meant by "these changes". :-) |
You're very welcome, and thank you for your prompt attention to it! |
1892da3 to
76a7875
Compare
I guess not. I have made #14031 for this. Please have a look. |
74f77e0 to
9752d71
Compare
c2b7f96 to
e22e456
Compare
|
Thanks for working on this patch @robinbb! I tried this in our codebase by adding a new (unused) module to a library which is close to the root of the dependency tree, and doing Do you have any insight as to what may be causing the extra recompilation? |
I think one case where unexpected extra recompilation is triggered is when there a library consisting of a single module. If memory serves there is an optimization that avoids calling |
|
Transparent module aliases hide library references from Here is the regression: This probably also occurs for any module from |
Continuing to investigate this, an triggers the recompilation of the |
e22e456 to
049e00a
Compare
4ff661e to
eecf3ac
Compare
|
Very cool work indeed! Is you new PR #14116 superseding this one? Either way, I think it's worth spending more time to improve the test suite, because it's easy to accidentally rely on subtle behaviors that are more permissive than the general case. For example, the Here's a small reproduction which should help debug the issue where a library re-exports one of its dependencies with a module alias. This cram test also shows the subtleties in the setup that could make it pass (when it shouldn't!) First we need a library where we'll perform the changes: Then another library, which exposes an alias to And a binary which depends on Compilation is different when the executable is defined in a single file, so the bug doesn't trigger unless we also add an empty unused file: (!!) The first build succeeds: Now we introduce a "soft" update, such that In that specific case, we are allowed to not recompile But if the Regarding revdeps testing, one issue to keep in mind is that testing a clean build (and not an incremental build) is likely to hide the failures: |
12e0764 to
d6ae7e8
Compare
No, not necessarily. I have 2 PRs because I need to test more than one thing at a time, and I need access to the CI invocations. (Very difficult to replicate the CI corner cases that are failing, now, for this effort. Non-corner cases are passing.) |
Yes, agreed - we need more tests. The absence of test coverage is what is making it so difficult to get this right. It is difficult to see just how many different scenarios are actually working (but non-optimal), in the status quo. I have submitted many augmentations already to the test suite in dependency PRs to this one (14017, 14031, 14100, 14101, 14103) in an effort to reduce the risk, already. More still are required. I see your advice for a suggested new test(s). I'll try to recreate that in a cram test. Possibly in a different PR (onto which I will rebase this branch). |
0cd6556 to
8d0f084
Compare
…14103) ## Summary Add baseline test documenting that single-module library consumers are unnecessarily recompiled when an unused dependency module changes. This happens because dune skips ocamldep for singleton stanzas, so per-module filtering cannot determine which libraries the module actually references. The code change to remove the singleton skip is deferred to the per-module filtering PR (#14021) where it is actually needed and can be tested end-to-end. Ref: #4572 ## Test plan - [x] `dune runtest test/blackbox-tests/test-cases/per-module-lib-deps/single-module-lib.t` passes Signed-off-by: Robin Bate Boerop <me@robinbb.com>
8d0f084 to
eecf3ac
Compare
…ml#4572) Add test verifying that when a library re-exports a dependency via a module alias (module Impl = Impl), incremental builds correctly recompile consumers when the re-exported library's .cmi changes. Soft changes (implementation only, no .cmi change) can safely skip recompilation due to -opaque. Hard changes (.cmi modified) must trigger recompilation to avoid inconsistent interface assumptions. Reproduction case from @art-w. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Move Hidden_deps (library file dependencies) out of Includes.make and into per-module computation in build_cm. This is a pure refactor with no behavioral change — all modules still depend on all libraries in the stanza's requires. This separation is necessary for issue ocaml#4572: Includes carries -I flags (which must be shared across all modules) while Hidden_deps can vary per-module. With this refactor, a future change can use ocamldep output to filter Hidden_deps per-module without affecting -I flags. - Includes.make no longer takes ~opaque or bundles Hidden_deps - New deps_of_entries in lib_file_deps.ml handles opaque logic - Alias and Wrapped_compat modules skip library deps (matching their existing Includes.empty behavior) - deps_with_exts removed (sole caller was the old opaque branch) Signed-off-by: Robin Bate Boerop <me@robinbb.com>
…issue ocaml#4572) Add a new function `read_immediate_deps_raw_of` to the ocamldep module that returns raw module names (as Module_name.Set.t) from ocamldep output, without resolving them to Module.t values. This function will be used to determine which external library modules a module actually references, enabling finer-grained dependency tracking where modules are only recompiled when libraries they actually use change. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Change deps_of_entries from Lib.t list to (Lib.t * Module.t option) list. Add deps_of_module (unused). Update all callers to pass (lib, None). No behavioral change — the None path is functionally identical. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Add Lib_index module to lib_file_deps.ml/mli. Not used anywhere yet. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Add lib_index field and accessor to Compilation_context. Wired up with Lib_index.create but not yet used in module_compilation.ml. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Split lib_cm_deps into lib_cm_deps_arg (static Command.Args.t) and lib_cm_deps_dyn (Action_builder, no-op for now). Add can_filter (always false). No behavioral change — lib_cm_deps_arg carries the same Hidden_deps as before, lib_cm_deps_dyn is a no-op. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Add real can_filter computation (Melange excluded, dep_graph_dir check, module kind check). The value is not used yet — lib_cm_deps_arg still provides static deps for all modules. This tests whether merely computing the can_filter conditions has side effects. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Enable can_filter and add full filtering logic in lib_cm_deps_dyn (Lib_index lookup, ocamldep refs, transitive deps, -open extraction). Static lib_cm_deps_arg remains for ALL modules — never removed. Both paths fire: modules get a superset of deps. The filtering optimization does not activate (tests unchanged), but the dynamic machinery runs for real. This tests whether the dyn_deps computation itself causes CI failures. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
c60087d to
652ef16
Compare
In the end, yes, it is. |
When can_filter is true, remove static Hidden_deps so dyn_deps becomes the sole library dep provider. Modules that don't reference a library are no longer recompiled when that library changes. Also includes -open flag extraction and -as-argument-for/-parameter module name handling to ensure filtering doesn't drop implicit deps. Test expectations updated. Reporting-of-cycles @package-cycle updated. Alias-reexport test updated to document known limitation. Resolves ocaml#4572 Signed-off-by: Robin Bate Boerop <me@robinbb.com>
652ef16 to
fe0ce36
Compare
|
Deprecated in favour of #14116 |
Implements per-module library dependency filtering as proposed in #4572. When
library A depends on library B, Dune currently assumes every module in A
depends on every module in B — causing full recompilation of A whenever any
module in B changes. This PR uses ocamldep's per-module output to declare
cross-library file dependencies only for libraries a module actually
references.
This PR builds on prerequisite PRs:
Hidden_depsfromIncludes.maketo per-module inbuild_cmDepends on #14030 and #14129.
The changes in this PR (on top of the above):
Lib_index(inlib_file_deps.ml) maps library entry module namesback to their libraries. It is computed once per stanza and stored in
Compilation_context.t.read_immediate_deps_raw_ofreads raw ocamldep output (includingcross-library references that were previously silently dropped), filters
out stanza-internal names, and looks up the remainder in the index to
determine which libraries are actually needed.
generated modules, and unresolvable references fall back to all-library
deps.
Resolves #4572