From e1128497c46256a09c87958f5274460bc0749a64 Mon Sep 17 00:00:00 2001 From: Robin Bate Boerop Date: Mon, 25 May 2026 14:05:13 -0700 Subject: [PATCH 1/4] feat: per-module narrowing for Melange consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts the [can_filter] Melange opt-out installed by #14516 (L4). The BFS narrowing pipeline now activates for Melange consumer compiles on the same terms as OCaml — same [Lib_index], same wrapped-lib soundness recovery, same [must_glob_set] / [tight_set] split. L4 routed Melange through [deps_of_entries] (broad) on the rationale that the narrowing had been developed and tested with OCaml-mode in mind. The defensive gate has no remaining technical justification: the cross-library walk reads [ocamldep] output (mode-agnostic), the classification pipeline is keyed on [Lib_index] entries (already populated for libs compiled in either mode), and the only adjustment needed in [deps_of_entry_modules] is a symmetric [want_cmj] arm to round out per-module deps for [Melange Cmj] consumers. [Module_compilation.lib_deps_for_module]: drop the [match Lib_mode.of_cm_kind cm_kind] arm from [can_filter]. The guard collapses to the four mode-independent predicates ([dep_graph] is real, the consumer module is in it, the module kind is filterable, the cctx is neither virtual nor a parameter). [Dep_rules.rules]: drop the [|| Compilation_mode.equal for_ Melange] disjunct from the singleton-stanza short-circuit. Single-module Melange stanzas with library deps now produce a real dep graph (the per-module filter consumes it). Comment reworded to drop the stale "Melange [can_filter]" forward-reference. [Lib_file_deps.deps_of_entry_modules]: add a [want_cmj] arm symmetric with [want_cmx]. For [cm_kind = Melange Cmj] consumers, per-module deps now include the dep lib's [.cmj] in addition to its [.cmi]; mirrors [groups_for_cm_kind]'s [Melange Cmj -> [Cmi; Cmj]] expansion used by the broad-dep path. The mli prose is rewritten — the "future Melange caller" caveat is gone, replaced with a positive description of all four [cm_kind] cases. [doc/dev/per-module-narrowing.md]: drop the two "Melange paths bypass narrowing" statements; the [can_filter] predicate list no longer includes an [Ocaml _] arm. Rewrite the trailing-section paragraph to describe Melange's [want_cmj] participation. Soundness: the only edge surfaced by this layer is the alias-form issue already documented by [wrapped-internal-leak.t] on L4 — a consumer referencing [Foo__Bar] directly via the wrapped-lib mangled-form alias. GitHub code search on 2026-05-25 found zero matches for [= Core__], [= Async__], [= Ppxlib__], [= Bonsai__] outside Melange's own test suite; the pattern is essentially absent in the OCaml ecosystem. Melange's [test/unit-tests/ounit_unicode_tests.ml] is the one known caller and is fixed in melange-re/melange to route through the supported [Melange_ppx.String_interp] wrapper. Tests: six existing Melange cram tests are promoted for output drift under the new path: - [depend-on-installed.t], [emit-installed.t], [emit-installed-two-modes.t], [emit-private.t]: +1 [ocamldep (internal)] line in [--display=short] trace. - [melange-conditional-modules.t]: +1 [ocamldep (internal)] + reorder of [ocamlc helper] vs [melc foo] in the build trace. No artefact change. - [missing-melc.t]: duplicated [melc not found] error block — the narrowing now triggers the consumer's per-module compile, which fails with the same melc-missing error. Exit code unchanged. Stack: rebases on #14521 (L9). Part of #14492. Signed-off-by: Robin Bate Boerop --- doc/dev/per-module-narrowing.md | 13 ++++---- src/dune_rules/dep_rules.ml | 9 +++--- src/dune_rules/lib_file_deps.ml | 31 +++++++++++++++---- src/dune_rules/lib_file_deps.mli | 10 +++--- src/dune_rules/module_compilation.ml | 5 +-- .../test-cases/melange/depend-on-installed.t | 1 + .../melange/emit-installed-two-modes.t | 1 + .../test-cases/melange/emit-installed.t | 1 + .../test-cases/melange/emit-private.t | 1 + .../melange/melange-conditional-modules.t | 3 +- .../test-cases/melange/missing-melc.t | 9 ++++++ 11 files changed, 56 insertions(+), 28 deletions(-) diff --git a/doc/dev/per-module-narrowing.md b/doc/dev/per-module-narrowing.md index 30720d1719f..d24d64d7d2f 100644 --- a/doc/dev/per-module-narrowing.md +++ b/doc/dev/per-module-narrowing.md @@ -45,8 +45,7 @@ the source-language kind (`Ml_kind.t` — `Impl` or `Intf`), and the mode 1. If a small set of preconditions fails (`can_filter = false`), fall back to the cctx-wide glob — the same dep set every compile rule would have had prior to this work. This avoids needing soundness - arguments for module kinds that the narrowing was not designed for, - and lets `Melange` paths pass through unchanged. + arguments for module kinds that the narrowing was not designed for. 2. Otherwise, if the consumer cctx already carries a virtual library in `requires` (`has_virtual_impl = true`), fall back to the @@ -95,8 +94,6 @@ The entry point for the narrowing is (`src/dune_rules/module_compilation.ml`). Before any narrowing runs, the function computes a `can_filter` boolean conjunction: -- The compile is `Ocaml _` (Melange has its own cm-kind story and is - not narrowed). - The supplied `dep_graph` is the real one for this cctx (its `dir` equals the cctx's `obj_dir`), not a `Dep_graph.dummy` — synthesised / link-time-generated / alias / root modules have no usable @@ -566,9 +563,11 @@ Three categories of fallback to the glob: inherited into the intermediate's `.mli` via the open) would silently drop the leaf library's `.cmi` from its compile rule. -`Melange` paths bypass the narrowing entirely at the `can_filter` -check; the cm_kind machinery there is different and L9 leaves it -unchanged. +`Melange` consumer compiles run the same narrowing pipeline as `Ocaml`. +The `can_filter` check is mode-agnostic; the only mode-specific code +is in `Lib_file_deps.deps_of_entry_modules`, which emits per-module +`.cmj` deps in addition to `.cmi` when `cm_kind = Melange Cmj`, +mirroring the broad-dep path's `groups_for_cm_kind`. ## Cost characteristics diff --git a/src/dune_rules/dep_rules.ml b/src/dune_rules/dep_rules.ml index 5dc2272211b..069f5b1e938 100644 --- a/src/dune_rules/dep_rules.ml +++ b/src/dune_rules/dep_rules.ml @@ -506,10 +506,11 @@ let for_module ~obj_dir ~modules ~sandbox ~impl ~dir ~sctx ~for_ module_ = let rules ~obj_dir ~modules ~sandbox ~impl ~sctx ~dir ~for_ ~has_library_deps = match Modules.With_vlib.as_singleton modules with - | Some m when (not has_library_deps) || Compilation_mode.equal for_ Melange -> - (* Single-module stanzas have no intra-stanza deps; the dep graph is only - consumed by the per-module filter in [lib_deps_for_module]. That filter - doesn't activate for Melange (see its [can_filter]) — skip ocamldep. *) + | Some m when not has_library_deps -> + (* Single-module stanzas with no library deps have nothing to filter — the + dep graph is only consumed by the per-module filter in + [lib_deps_for_module], and that filter has no libs to narrow. Skip + ocamldep. *) Memo.return (Dep_graph.Ml_kind.dummy m) | Some _ | None -> let transitive_deps, imported_vlib_deps = diff --git a/src/dune_rules/lib_file_deps.ml b/src/dune_rules/lib_file_deps.ml index 875060da506..a6c2cc1ac8e 100644 --- a/src/dune_rules/lib_file_deps.ml +++ b/src/dune_rules/lib_file_deps.ml @@ -85,6 +85,11 @@ let deps_of_entry_modules ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) lib modules = | Ocaml Cmx -> not (opaque && Lib.is_local lib) | _ -> false in + let want_cmj = + match cm_kind with + | Melange Cmj -> true + | _ -> false + in List.fold_left modules ~init:Dep.Set.empty ~f:(fun acc m -> let acc = match Obj_dir.Module.cm_public_file obj_dir m ~kind:cmi_kind with @@ -97,16 +102,30 @@ let deps_of_entry_modules ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) lib modules = in tight_modules" [ "module", Module.to_dyn m; "lib", Lib.to_dyn lib ] in - if want_cmx && Module.has m ~ml_kind:Impl + let acc = + if want_cmx && Module.has m ~ml_kind:Impl + then ( + match Obj_dir.Module.cm_public_file obj_dir m ~kind:(Ocaml Cmx) with + | Some path -> Dep.Set.add acc (Dep.file path) + | None -> + (* Unreachable. [cm_public_file ~kind:(Ocaml Cmx)] returns [None] only + when [not has_impl]. The enclosing [if] guarantees + [Module.has m ~ml_kind:Impl] (= [has_impl]). *) + Code_error.raise + "deps_of_entry_modules: [cm_public_file] returned [None] for cmx despite \ + [Module.has m ~ml_kind:Impl] holding" + [ "module", Module.to_dyn m ]) + else acc + in + if want_cmj && Module.has m ~ml_kind:Impl then ( - match Obj_dir.Module.cm_public_file obj_dir m ~kind:(Ocaml Cmx) with + match Obj_dir.Module.cm_public_file obj_dir m ~kind:(Melange Cmj) with | Some path -> Dep.Set.add acc (Dep.file path) | None -> - (* Unreachable. [cm_public_file ~kind:(Ocaml Cmx)] returns [None] only - when [not has_impl]. The enclosing [if] guarantees - [Module.has m ~ml_kind:Impl] (= [has_impl]). *) + (* Unreachable. Symmetric with the [Ocaml Cmx] arm above: [cm_public_file + ~kind:(Melange Cmj)] returns [None] only when [not has_impl]. *) Code_error.raise - "deps_of_entry_modules: [cm_public_file] returned [None] for cmx despite \ + "deps_of_entry_modules: [cm_public_file] returned [None] for cmj despite \ [Module.has m ~ml_kind:Impl] holding" [ "module", Module.to_dyn m ]) else acc) diff --git a/src/dune_rules/lib_file_deps.mli b/src/dune_rules/lib_file_deps.mli index 876a99fa4b1..3f401845b9a 100644 --- a/src/dune_rules/lib_file_deps.mli +++ b/src/dune_rules/lib_file_deps.mli @@ -22,12 +22,10 @@ val deps_of_entries : opaque:bool -> cm_kind:Lib_mode.Cm_kind.t -> Lib.t list -> (** Specific-file deps on the [modules] of [lib]. Only valid for local libraries (where [Module.t] values are available). - Currently produces complete per-module deps only for [cm_kind = Ocaml _] - (cmi + cmx); for [Melange _] only the cmi is emitted — there is no - per-module cmj arm, asymmetric with [deps_of_entries]. The sole caller - ([Module_compilation.lib_deps_for_module]) gates Melange out before - reaching this function, so this asymmetry is not observable today. If a - future Melange caller is added, extend with a [want_cmj] arm. *) + Per [cm_kind]: [Ocaml Cmx] emits cmi + cmx (cmx omitted when [opaque] and + [Lib.is_local lib]); [Ocaml (Cmi | Cmo)] emits cmi only; [Melange Cmj] + emits cmi + cmj; [Melange Cmi] emits cmi only. The cmi/cmj selection + mirrors [groups_for_cm_kind] used by the broad-dep path. *) val deps_of_entry_modules : opaque:bool -> cm_kind:Lib_mode.Cm_kind.t diff --git a/src/dune_rules/module_compilation.ml b/src/dune_rules/module_compilation.ml index db7286e3f1f..805e73e0efe 100644 --- a/src/dune_rules/module_compilation.ml +++ b/src/dune_rules/module_compilation.ml @@ -90,13 +90,10 @@ let lib_deps_for_module ~cctx ~obj_dir ~for_ ~dep_graph ~opaque ~cm_kind ~ml_kin Lib_mode.Cm_kind.Map.get (Compilation_context.includes cctx) cm_kind in let can_filter = - (match Lib_mode.of_cm_kind cm_kind with - | Melange -> false - | Ocaml _ -> true) (* Skip when [dep_graph] is the dummy ([Dep_graph.dummy] with [dir = Path.Build.root]); used for singleton-module stanzas and link-time-synthesised modules where no transitive deps are available. *) - && Path.Build.equal (Dep_graph.dir dep_graph) (Obj_dir.dir obj_dir) + Path.Build.equal (Dep_graph.dir dep_graph) (Obj_dir.dir obj_dir) (* Modules synthesised outside the stanza, handed to [ocamlc_i]. *) && Dep_graph.mem dep_graph m && module_kind_is_filterable m diff --git a/test/blackbox-tests/test-cases/melange/depend-on-installed.t b/test/blackbox-tests/test-cases/melange/depend-on-installed.t index 2b10029f31d..e2f4828e0d5 100644 --- a/test/blackbox-tests/test-cases/melange/depend-on-installed.t +++ b/test/blackbox-tests/test-cases/melange/depend-on-installed.t @@ -57,6 +57,7 @@ Test dependency on installed package ocamldep (internal) ocamldep (internal) melc .b.objs/melange/b.{cmi,cmj,cmt} + ocamldep (internal) melc .b.objs/melange/b__Bar.{cmi,cmj,cmt} melc .b.objs/melange/b__Foo.{cmi,cmj,cmt} Leaving directory 'b' diff --git a/test/blackbox-tests/test-cases/melange/emit-installed-two-modes.t b/test/blackbox-tests/test-cases/melange/emit-installed-two-modes.t index cc5ea6e5849..da8597cf2f8 100644 --- a/test/blackbox-tests/test-cases/melange/emit-installed-two-modes.t +++ b/test/blackbox-tests/test-cases/melange/emit-installed-two-modes.t @@ -59,6 +59,7 @@ Test dependency on installed package Entering directory 'b' melc dist/node_modules/a/a.js melc dist/node_modules/a/foo.js + ocamldep (internal) melc dist/bar.js Leaving directory 'b' diff --git a/test/blackbox-tests/test-cases/melange/emit-installed.t b/test/blackbox-tests/test-cases/melange/emit-installed.t index 5d5b624ba5c..f899fa4a99f 100644 --- a/test/blackbox-tests/test-cases/melange/emit-installed.t +++ b/test/blackbox-tests/test-cases/melange/emit-installed.t @@ -62,6 +62,7 @@ Test dependency on installed package melc dist/node_modules/a/a.js melc dist/node_modules/a/foo.js melc dist/node_modules/a/sub/sub.js + ocamldep (internal) melc dist/bar.js Leaving directory 'b' diff --git a/test/blackbox-tests/test-cases/melange/emit-private.t b/test/blackbox-tests/test-cases/melange/emit-private.t index 6574b63baf5..6ffd5365e6c 100644 --- a/test/blackbox-tests/test-cases/melange/emit-private.t +++ b/test/blackbox-tests/test-cases/melange/emit-private.t @@ -47,6 +47,7 @@ Test dependency on a private library in the same package as melange.emit > EOF $ OCAMLPATH=$PWD/prefix/lib/:$OCAMLPATH dune build @dist --display=short 2>&1 | grep -v melange + ocamldep (internal) melc b/dist/node_modules/pkg.__private__.a/foo.js melc b/dist/b/bar.js diff --git a/test/blackbox-tests/test-cases/melange/melange-conditional-modules.t b/test/blackbox-tests/test-cases/melange/melange-conditional-modules.t index 98bae15313a..22a1a0d15b0 100644 --- a/test/blackbox-tests/test-cases/melange/melange-conditional-modules.t +++ b/test/blackbox-tests/test-cases/melange/melange-conditional-modules.t @@ -34,8 +34,9 @@ Show `(melange.modules ..)` subset is preferred when compiling in Melange mode melc lib/.a.objs/melange/a.{cmi,cmj,cmt} ocamldep (internal) ocamldep (internal) - melc lib/.a.objs/melange/a__Foo.{cmi,cmj,cmt} + ocamldep (internal) ocamlc lib/.a.objs/byte/a__Helper.{cmi,cmo,cmt} + melc lib/.a.objs/melange/a__Foo.{cmi,cmj,cmt} ocamlc lib/.a.objs/byte/a__Foo.{cmi,cmo,cmt} ocamlc lib/a.cma Leaving directory 'a' diff --git a/test/blackbox-tests/test-cases/melange/missing-melc.t b/test/blackbox-tests/test-cases/melange/missing-melc.t index 84b24ffb3a8..04e9063b527 100644 --- a/test/blackbox-tests/test-cases/melange/missing-melc.t +++ b/test/blackbox-tests/test-cases/melange/missing-melc.t @@ -88,4 +88,13 @@ But trying to build any melange artifacts will fail Error: Program melc not found in the tree or in PATH (context: default) Hint: opam install melange + File "dune", lines 10-14, characters 0-94: + 10 | (melange.emit + 11 | (target output) + 12 | (emit_stdlib false) + 13 | (modules main_melange) + 14 | (libraries lib1)) + Error: Program melc not found in the tree or in PATH + (context: default) + Hint: opam install melange [1] From ba434cf9a5b2a158a681d681f6c1d2b6b9b0a15e Mon Sep 17 00:00:00 2001 From: Robin Bate Boerop Date: Mon, 25 May 2026 14:28:43 -0700 Subject: [PATCH 2/4] fix: thread pps_runtime_libs through melange.emit cctx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Melange_rules.compile_rules_for_target] was passing [~pps_runtime_libs:(Resolve.Memo.return [])] as a placeholder. Pre-L10 this was inert because the Melange compile path skipped narrowing entirely. With the gate-lift in this PR, the narrowing pipeline now reads [Compilation_context.pps_runtime_libs] and force-globs those libs into [must_glob_set] — but the empty placeholder dropped every ppx_runtime_libs declaration on melange.emit stanzas. Symptom: [melange-pps-runtime-libs.t] under [per-module-lib-deps/] loses the [*.cmi]/[*.cmj] globs over the runtime lib's melange objdir, exposing a soundness regression: a [(melange.emit ...)] stanza preprocessing with a ppx whose [(ppx_runtime_libraries ...)] points at a lib that's never named in source would silently drop that lib's cmi/cmj from the compile-rule deps. Compute [pps_runtime_libs] from [compile_info] the same way [Lib_rules] and [Exe_rules] do: let* pps = Lib.Compile.pps compile_info ~for_ in Resolve.Memo.List.concat_map pps ~f:(Lib.ppx_runtime_deps ~for_) The L5 soundness recovery for ppx_runtime_libs now applies to Melange consumers as well. Signed-off-by: Robin Bate Boerop --- src/dune_rules/melange/melange_rules.ml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dune_rules/melange/melange_rules.ml b/src/dune_rules/melange/melange_rules.ml index ef01410afce..49459f090cc 100644 --- a/src/dune_rules/melange/melange_rules.ml +++ b/src/dune_rules/melange/melange_rules.ml @@ -542,6 +542,11 @@ let setup_emit_cmj_rules Modules.With_vlib.modules modules, pp in let requires_link = Lib.Compile.requires_link compile_info ~for_ in + let pps_runtime_libs = + let open Resolve.Memo.O in + let* pps = Lib.Compile.pps compile_info ~for_ in + Resolve.Memo.List.concat_map pps ~f:(Lib.ppx_runtime_deps ~for_) + in let* flags = melange_compile_flags ~sctx ~dir mel in let* cctx = let direct_requires = Lib.Compile.direct_requires compile_info ~for_ in @@ -555,7 +560,7 @@ let setup_emit_cmj_rules ~flags ~requires_link ~requires_compile:direct_requires - ~pps_runtime_libs:(Resolve.Memo.return []) + ~pps_runtime_libs ~preprocessing:pp ~js_of_ocaml:(Js_of_ocaml.Mode.Pair.make None) ~opaque:Inherit_from_settings From 731495e54754b59916709397bd553d58d582c172 Mon Sep 17 00:00:00 2001 From: Robin Bate Boerop Date: Mon, 25 May 2026 16:31:04 -0700 Subject: [PATCH 3/4] fix: keep [Stdlib]-provider lib in [kept_libs] under narrowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every OCaml/Melange compile implicitly opens [Stdlib] before reading its source. The implicit open is not visible in [ocamldep -modules] output (ocamldep reports only modules named in source), so it never appears in the BFS frontier and the lib that provides [Stdlib] is dropped from [kept_libs] under L6's filtered-include-flags path. For OCaml-mode compiles this is typically invisible: [Stdlib] lives on the compiler's built-in include path (via [+stdlib]); no dune-managed lib provides a module named [Stdlib], so the [Lib_index] lookup for [Stdlib] is empty and no narrowing decision depends on it. For Melange-mode compiles (post-L10), [Stdlib] lives in the [melange]/[stdlib] dune library, and dropping its [-I] from a consumer's compile rule causes [Error: Unbound module Stdlib] at the [-open] resolution step the moment that consumer's source references any [Stdlib] type ([int], [float], etc.) without syntactically naming a [Stdlib.X] member. Force [Stdlib] onto the initial BFS frontier in [lib_deps_for_module] alongside [all_raw] and [open_modules]. The injection is mode-agnostic — the [Lib_index] lookup decides what (if anything) to include: - Typical OCaml project: no dune lib named [Stdlib], lookup is empty, zero effect. - Melange project: pulls in the [melange]/[stdlib] lib via the normal classification path. Fixes the Stdlib-resolution bug. - Unwrapped OCaml lib that exports a [Stdlib] module (rare): the lookup hits and that lib's [-I] + [Stdlib.cmi] are kept in every consumer's compile rule. Matches the broad-dep path's behavior pre-narrowing — slight precision loss (extra dep) but soundness preserved. - Wrapped lib with a [Stdlib.ml] sibling: index records only the wrapper name, lookup is empty, no effect. Reproduced and fixed against: - belt unit lib (melange 6.0.1-54): rebuild from scratch under L10 dune now produces all [belt__Belt_*.cmi]/[cmj] artefacts (was failing with [Error: Unbound module Stdlib] on every [.pp.mli] whose source did not syntactically name [Stdlib.X]). - ocaml/dune#14732 CI: macOS opam [Install deps] step failed compiling melange.6.0.1-54 with hundreds of identical errors, same root cause. Signed-off-by: Robin Bate Boerop --- src/dune_rules/module_compilation.ml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/dune_rules/module_compilation.ml b/src/dune_rules/module_compilation.ml index 805e73e0efe..c5342e6d7a9 100644 --- a/src/dune_rules/module_compilation.ml +++ b/src/dune_rules/module_compilation.ml @@ -180,7 +180,22 @@ let lib_deps_for_module ~cctx ~obj_dir ~for_ ~dep_graph ~opaque ~cm_kind ~ml_kin let all_raw = Module_name.Set.union m_raw trans_raw in let* flags = Ocaml_flags.get (Compilation_context.flags cctx) mode in let open_modules = Ocaml_flags.extract_open_module_names flags in - let referenced = Module_name.Set.union all_raw open_modules in + (* [Stdlib] is implicitly opened by every compile and so is referenced by + every consumer even when ocamldep doesn't list it. For OCaml-mode + compiles the compiler's built-in stdlib path supplies it (no dune lib + to keep); for Melange-mode compiles the [melange]/[stdlib] lib supplies + it as a regular dune dependency, and would otherwise be dropped from + [kept_libs] for consumers that don't syntactically name a [Stdlib.X] + member. Force it onto the frontier so the [Lib_index] lookup adds it + when present. *) + let referenced = + let implicit_stdlib = + match Module_name.of_string_opt "Stdlib" with + | Some n -> Module_name.Set.singleton n + | None -> Module_name.Set.empty + in + Module_name.Set.union (Module_name.Set.union all_raw open_modules) implicit_stdlib + in let { Lib_file_deps.Lib_index.tight; non_tight } = Lib_file_deps.Lib_index.filter_libs_with_modules lib_index From cfe0572c77b00be348b889474e676c1c5d0e851f Mon Sep 17 00:00:00 2001 From: Robin Bate Boerop Date: Tue, 26 May 2026 10:51:35 -0700 Subject: [PATCH 4/4] doc: list both [cm_kind] match sites in narrowing reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Melange paragraph claimed the only mode-aware code was in [Lib_file_deps.deps_of_entry_modules], but [need_impl_deps_of] (inside [Module_compilation.lib_deps_for_module]) also matches on [cm_kind] and explicitly names [Melange _] in the pattern. The behaviour there is "Cmx reads trans-dep impl ocamldep; everything else does not" — [Melange _] is in the same bucket as [Ocaml (Cmi | Cmo)], not its own — but the doc oversimplified to the point of inaccuracy. Rewrite as a numbered list naming both [cm_kind] match sites and clarifying that the second site's distinction is Cmx-vs-rest, not OCaml-vs-Melange. Replies to ocaml/dune#14732 review comment r3304097840 (Copilot). Signed-off-by: Robin Bate Boerop --- doc/dev/per-module-narrowing.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/dev/per-module-narrowing.md b/doc/dev/per-module-narrowing.md index d24d64d7d2f..ae863b2330e 100644 --- a/doc/dev/per-module-narrowing.md +++ b/doc/dev/per-module-narrowing.md @@ -564,10 +564,17 @@ Three categories of fallback to the glob: silently drop the leaf library's `.cmi` from its compile rule. `Melange` consumer compiles run the same narrowing pipeline as `Ocaml`. -The `can_filter` check is mode-agnostic; the only mode-specific code -is in `Lib_file_deps.deps_of_entry_modules`, which emits per-module -`.cmj` deps in addition to `.cmi` when `cm_kind = Melange Cmj`, -mirroring the broad-dep path's `groups_for_cm_kind`. +The `can_filter` check is mode-agnostic. Two code paths inside the +pipeline match on `cm_kind`: + +1. `Lib_file_deps.deps_of_entry_modules` emits per-module `.cmj` in + addition to `.cmi` when `cm_kind = Melange Cmj`, mirroring the + broad-dep path's `groups_for_cm_kind`. +2. `need_impl_deps_of` (in `lib_deps_for_module`) reads a trans-dep's + `.ml`-side ocamldep only for `cm_kind = Ocaml Cmx` (cross-module + inlining input). `Ocaml (Cmi | Cmo)` and `Melange _` are handled + symmetrically — neither reads impl deps. The distinction is + Cmx-vs-rest, not OCaml-vs-Melange. ## Cost characteristics