Skip to content

Fix DCE false positives for functors constrained by a named module type - #216

Closed
JonoPrest wants to merge 4 commits into
rescript-lang:masterfrom
JonoPrest:jono-fix-functor-false-positive
Closed

JonoPrest wants to merge 4 commits into
rescript-lang:masterfrom
JonoPrest:jono-fix-functor-false-positive

Conversation

@JonoPrest

Copy link
Copy Markdown
Contributor

When a functor result is constrained to a module type defined in another file, every value in the functor body is reported dead even when instances are used from live code.

(* gen.ml *)
module type S = sig
  val find_opt : int -> int option
  val unused_in_sig : int -> int
end

(* hash.mli *)
module Make (K : sig type t end) : Gen.S

(* hash.ml *)
module Make (K : sig type t end) = struct
  let find_opt k = Some k                                                                                                                                                     let unused_in_sig k = k
  let truly_dead k = k                                                                                                                                                      end
                                                                                                                                                                            (* main.ml *)
module H = Hash.Make (struct type t = int end)                                                                                                                              let () = ignore (H.find_opt 1)

Before: find_opt and hash.Make are reported dead. Expected: only unused_in_sig and truly_dead.
On OCaml 5.3+ the find_opt binding depends on the uid of val find_opt in gen.ml, but the dependency extractor only loaded the local .cmt/.cmti, so the link was dropped.
Fix:

  • Index all cmt files by compilation unit and resolve foreign uids from them. Same-named units (e.g. unwrapped libraries in different directories) are disambiguated by the importer's interface digest; if that fails, every candidate is consulted opped.
  • Process value dependencies after all files are scanned. When the signature side is a val in a module type rather than a declaration, forward its references onto the
    implementation, so unused signature items are still reported.

Fixtures cover .mli and inline constraints, plus a duplicate-unit regre

Values in a functor body whose result is constrained to a module type
defined in another module (via .mli or inline) are reported dead even
when instances are used from live code.
When a module or functor result is constrained to a module type defined
in another file (via .mli or inline), the typed tree's declaration
dependencies link each implementation value to the `val` item of that
module type, which lives in another compilation unit's cmt. The
extractor only loaded the local cmt and cmti, so those links were dropped
and every value in the functor body was reported dead.

- Index all cmt/cmti files by compilation unit up front and resolve
  foreign uids from them (OCaml 5.3+).
- Defer processing of value dependencies until all files are scanned.
  When the signature side is not a declaration (a `val` inside a named
  module type), forward the references made to it onto the
  implementation rather than treating it as an always-live reference,
  so unused items in the signature are still reported.
- In ReScript project mode, collect the cmt files of every source
  directory before processing any, so declaration dependencies across
  directories resolve.
- When forwarding references from a module-type val item onto its
  implementation, also forward the recorded optional-argument calls.
  Otherwise a call like H.f ~x:1 () through a constrained functor result
  would report x as never used. Covered by with_opt in the fixture.
The analysis root can contain several units with the same name (e.g.
unwrapped libraries in different directories). Their declarations share
uids, so merging every candidate into one table first-one-wins could
resolve a functor's module type constraint to the wrong unit and leave a
used value reported dead.

Cross-unit lookups now keep one declaration table per candidate file,
narrow the candidates down using the interface digest the importer
recorded in cmt_imports, and consult every remaining candidate when the
digest cannot disambiguate.

Adds a regression fixture with two unwrapped libraries defining Dup_sig.
@JonoPrest
JonoPrest force-pushed the jono-fix-functor-false-positive branch from 6a83d4b to dc2d4de Compare September 17, 2026 10:12
@JonoPrest

Copy link
Copy Markdown
Contributor Author

@codex review

@JonoPrest

Copy link
Copy Markdown
Contributor Author

I can't stack PRs from my fork owned branches so closed for #217

@JonoPrest JonoPrest closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant