Skip to content

Commit 8c03985

Browse files
committed
Fix DCE false positives for functors constrained by a named module type
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.
1 parent 5b18e1c commit 8c03985

4 files changed

Lines changed: 96 additions & 8 deletions

File tree

Changes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
3+
- Fix false positives for values in functor bodies (and modules) constrained by a module type defined in another file, e.g. `module Make (K) : Gen.S with type key = K.t`. On OCaml 5.3+ declaration dependencies pointing at other compilation units are now resolved, and references to `val` items of a named module type are forwarded to the implementations satisfying it.
4+
15
# 2.26.0
26

37
- Support OCaml 5.3, 5.4, and 5.5.

src/Compat.ml

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,26 +303,80 @@ let applyArgOfExpression e =
303303
Some e
304304
#endif
305305

306+
(* Index of the .cmt/.cmti files under analysis, keyed by compilation unit
307+
name. Populated before processing so declaration dependencies pointing at
308+
other units (e.g. a functor result constrained by a module type defined in
309+
another file) can be resolved regardless of processing order. *)
310+
let cmtFilesByUnit : (string, string list) Hashtbl.t = Hashtbl.create 256
311+
312+
let unitNameOfCmtFile path =
313+
path |> Filename.basename |> Filename.remove_extension
314+
|> String.capitalize_ascii
315+
316+
let registerCmtFile path =
317+
let unit = unitNameOfCmtFile path in
318+
let existing =
319+
match Hashtbl.find_opt cmtFilesByUnit unit with
320+
| Some paths -> paths
321+
| None -> []
322+
in
323+
if not (List.mem path existing) then
324+
Hashtbl.replace cmtFilesByUnit unit (path :: existing)
325+
306326
let extractValueDependencies ~cmtFilePath (cmt_infos : Cmt_format.cmt_infos) =
307327
#if OCAML_VERSION >= (5, 3, 0)
308328
let module UidTbl = Shape.Uid.Tbl in
309329
let uid_to_decl = UidTbl.create 1024 in
310330
UidTbl.iter (UidTbl.replace uid_to_decl) cmt_infos.cmt_uid_to_decl;
331+
let loadedFiles = Hashtbl.create 16 in
311332
let add_uid_to_decl_from_cmt path =
312-
if Sys.file_exists path then
333+
if (not (Hashtbl.mem loadedFiles path)) && Sys.file_exists path then (
334+
Hashtbl.replace loadedFiles path ();
313335
try
314336
let cmt_infos = Cmt_format.read_cmt path in
315-
UidTbl.iter (UidTbl.replace uid_to_decl) cmt_infos.cmt_uid_to_decl
316-
with _ -> ()
337+
UidTbl.iter
338+
(fun uid decl ->
339+
if not (UidTbl.mem uid_to_decl uid) then
340+
UidTbl.replace uid_to_decl uid decl)
341+
cmt_infos.cmt_uid_to_decl
342+
with _ -> ())
317343
in
344+
Hashtbl.replace loadedFiles cmtFilePath ();
318345
add_uid_to_decl_from_cmt
319346
((cmtFilePath |> Filename.remove_extension) ^ ".cmti");
347+
let loadedUnits = Hashtbl.create 16 in
348+
let load_unit comp_unit =
349+
if not (Hashtbl.mem loadedUnits comp_unit) then (
350+
Hashtbl.replace loadedUnits comp_unit ();
351+
let indexed =
352+
match Hashtbl.find_opt cmtFilesByUnit comp_unit with
353+
| Some paths -> paths
354+
| None -> []
355+
in
356+
(* Fall back to sibling files, for callers that did not register. *)
357+
let dir = Filename.dirname cmtFilePath in
358+
let siblings =
359+
[".cmt"; ".cmti"]
360+
|> List.concat_map (fun ext ->
361+
[
362+
Filename.concat dir (comp_unit ^ ext);
363+
Filename.concat dir (String.uncapitalize_ascii comp_unit ^ ext);
364+
])
365+
in
366+
List.iter add_uid_to_decl_from_cmt (indexed @ siblings))
367+
in
320368
let loc_of_value_decl = function
321369
| Typedtree.Value {val_loc; _} -> Some val_loc
322370
| Typedtree.Value_binding {vb_pat = {pat_loc; _}; _} -> Some pat_loc
323371
| _ -> None
324372
in
325373
let loc_of_uid uid =
374+
(match UidTbl.find_opt uid_to_decl uid with
375+
| None -> (
376+
match uid with
377+
| Shape.Uid.Item {comp_unit; _} -> load_unit comp_unit
378+
| _ -> ())
379+
| Some _ -> ());
326380
match UidTbl.find_opt uid_to_decl uid with
327381
| Some item_decl -> loc_of_value_decl item_decl
328382
| None -> None

src/DeadValue.ml

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -509,14 +509,42 @@ let processValueDependency
509509
({loc_start = {pos_fname = fnFrom} as posFrom; loc_ghost = ghost2} as
510510
locFrom :
511511
Location.t) ) =
512-
if (not ghost1) && (not ghost2) && posTo <> posFrom then (
513-
let addFileReference = fileIsImplementationOf fnTo fnFrom in
514-
addValueReference ~addFileReference ~locFrom ~locTo;
515-
DeadOptionalArgs.addFunctionReference ~locFrom ~locTo)
512+
if (not ghost1) && (not ghost2) && posTo <> posFrom then
513+
match PosHash.find_opt decls posFrom with
514+
| None ->
515+
(* The signature item is not a declaration (e.g. a [val] inside a named
516+
module type used to constrain a module or functor result), so it can
517+
never be resolved as dead. Forward the references made to the
518+
signature item onto the implementation instead. *)
519+
ValueReferences.find posFrom
520+
|> PosSet.iter (fun posRef ->
521+
if posRef <> posTo then
522+
let locRef =
523+
{
524+
Location.loc_start = posRef;
525+
loc_end = posRef;
526+
loc_ghost = false;
527+
}
528+
in
529+
addValueReference ~addFileReference:true ~locFrom:locRef ~locTo)
530+
| Some _ ->
531+
let addFileReference = fileIsImplementationOf fnTo fnFrom in
532+
addValueReference ~addFileReference ~locFrom ~locTo;
533+
DeadOptionalArgs.addFunctionReference ~locFrom ~locTo
534+
535+
(* Value dependencies are processed once all files have been scanned, so that
536+
declarations and references from every file are known. *)
537+
let delayedValueDependencies = ref []
538+
539+
let forceDelayedItems () =
540+
let dependencies = List.rev !delayedValueDependencies in
541+
delayedValueDependencies := [];
542+
dependencies |> List.iter processValueDependency
516543

517544
let processStructure ~cmt_value_dependencies ~doTypes ~doExternals
518545
(structure : Typedtree.structure) =
519546
let traverseStructure = traverseStructure ~doTypes ~doExternals in
520547
structure |> traverseStructure.structure traverseStructure |> ignore;
521548
let valueDependencies = cmt_value_dependencies |> List.rev in
522-
valueDependencies |> List.iter processValueDependency
549+
delayedValueDependencies :=
550+
List.rev_append valueDependencies !delayedValueDependencies

src/Reanalyze.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ let processCmtFiles ~cmtRoot =
4343
Filename.check_suffix path ".cmt" || Filename.check_suffix path ".cmti"
4444
in
4545
let processCmtFilePaths cmtFilePaths =
46+
cmtFilePaths |> List.iter Compat.registerCmtFile;
4647
cmtFilePaths |> List.iter (loadCmtFile ~cmtRoot)
4748
in
4849
match cmtRoot with
@@ -88,6 +89,7 @@ let runAnalysis ~cmtRoot ~ppf =
8889

8990
processCmtFiles ~cmtRoot;
9091
if runConfig.dce then (
92+
DeadValue.forceDelayedItems ();
9193
DeadException.forceDelayedItems ();
9294
DeadOptionalArgs.forceDelayedItems ();
9395
DeadCommon.reportDead ~checkOptionalArg:DeadOptionalArgs.check ppf;

0 commit comments

Comments
 (0)