Skip to content

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

Merged
JonoPrest merged 4 commits into
masterfrom
jono-fix-functor-false-positive
Sep 18, 2026
Merged

JonoPrest merged 4 commits into
masterfrom
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

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-17T10:42:18.407118Z dc2d4de Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc2d4de2bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/DeadValue.ml
never be resolved as dead. Forward the references made to the
signature item onto the implementation instead. *)
DeadOptionalArgs.forwardDelayedItems ~posFrom ~posTo;
ValueReferences.find posFrom

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope named-signature references to one implementation

On OCaml 5.3+, when two functors or modules are constrained by the same named module type, references such as H1.f and H2.f share the source position of that module type's val f. Every implementation dependency performs this global lookup, so a call through H1.f is replayed onto both implementations and an unused H2.f is incorrectly considered live. Preserve the originating constrained module or application when forwarding instead of broadcasting every reference stored at the shared signature position.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware of this and that's what #218 is for. The scope of this PR is just to silence false positives.

@JonoPrest

Copy link
Copy Markdown
Contributor Author

@cristianoc please note the stacked pr on top of this. Regarding the codex comment.

@JonoPrest
JonoPrest merged commit ee4115d into master Sep 18, 2026
14 checks passed
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