From f078a98cf9570e75cf7fbce3882b6e23a0e1701d Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Mon, 8 Dec 2025 05:53:15 +0100 Subject: [PATCH] DCE: Tasks 5 & 6 - References and CrossFileItems patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the map → list → merge pattern to references and cross-file items. ## Task 5: References New module: References.ml/.mli - builder (mutable) for AST processing - t (immutable) for solver - Tracks both value refs and type refs - PosSet for position sets Changes: - Thread ~refs:References.builder through AST processing - addValueReference, addTypeReference use References API - Solver uses References.find_value_refs, References.find_type_refs - Deleted global ValueReferences.table and TypeReferences.table ## Task 6: CrossFileItems (renamed from DelayedItems) New module: CrossFileItems.ml/.mli - builder (mutable) for AST processing - t (immutable) for processing after merge - Three item types: exception_refs, optional_arg_calls, function_refs Changes: - Thread ~cross_file:CrossFileItems.builder through AST processing - DeadException.markAsUsed adds to cross_file builder - DeadOptionalArgs.addReferences, addFunctionReference add to cross_file - Deleted global delayedItems refs from DeadException and DeadOptionalArgs ## Data flow process_cmt_file (per-file) → file_data { annotations; decls; refs; cross_file } Merge phase: FileAnnotations.merge_all → annotations (immutable) Declarations.merge_all → decls (immutable) CrossFileItems.merge_all → cross_file (immutable) References builders merged into refs_builder Process cross-file items: process_exception_refs → writes to refs_builder process_optional_args → reads decls Freeze: refs_builder → refs (immutable) Solver: reportDead ~annotations ~decls ~refs ## Global state deleted - DeadCommon.ValueReferences.table - DeadCommon.TypeReferences.table - DeadException.delayedItems - DeadOptionalArgs.delayedItems - DeadOptionalArgs.functionReferences ## Naming Renamed DelayedItems → CrossFileItems because it better describes the semantic meaning: items that reference across file boundaries. --- analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md | 73 ++++++++++++---- analysis/reanalyze/src/CrossFileItems.ml | 92 ++++++++++++++++++++ analysis/reanalyze/src/CrossFileItems.mli | 52 +++++++++++ analysis/reanalyze/src/DceFileProcessing.ml | 12 ++- analysis/reanalyze/src/DceFileProcessing.mli | 4 +- analysis/reanalyze/src/DeadCommon.ml | 59 +++++-------- analysis/reanalyze/src/DeadException.ml | 26 ++---- analysis/reanalyze/src/DeadOptionalArgs.ml | 44 ++-------- analysis/reanalyze/src/DeadType.ml | 12 +-- analysis/reanalyze/src/DeadValue.ml | 66 ++++++++------ analysis/reanalyze/src/Reanalyze.ml | 22 ++++- analysis/reanalyze/src/References.ml | 75 ++++++++++++++++ analysis/reanalyze/src/References.mli | 42 +++++++++ 13 files changed, 431 insertions(+), 148 deletions(-) create mode 100644 analysis/reanalyze/src/CrossFileItems.ml create mode 100644 analysis/reanalyze/src/CrossFileItems.mli create mode 100644 analysis/reanalyze/src/References.ml create mode 100644 analysis/reanalyze/src/References.mli diff --git a/analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md b/analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md index d173b47041..08bdfe76c5 100644 --- a/analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md +++ b/analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md @@ -80,11 +80,11 @@ you can swap one file's data without affecting others. **Impact**: Can't analyze a subset of files without reanalyzing everything. Can't clear state between test runs without module reloading. -### P3: Delayed/deferred processing queues +### P3: Cross-file processing queues **Problem**: Several analyses use global queues that get "flushed" later: -- `DeadOptionalArgs.delayedItems` - deferred optional arg analysis -- `DeadException.delayedItems` - deferred exception checks -- `DeadType.TypeDependencies.delayedItems` - deferred type deps +- `DeadOptionalArgs.delayedItems` - cross-file optional arg analysis → DELETED (now `CrossFileItems`) +- `DeadException.delayedItems` - cross-file exception checks → DELETED (now `CrossFileItems`) +- `DeadType.TypeDependencies.delayedItems` - per-file type deps (already handled per-file) - `ProcessDeadAnnotations.positionsAnnotated` - annotation tracking **Additional problem**: `positionsAnnotated` mixes **input** (source annotations from AST) with **output** (positions the solver determines are dead). The solver mutates this during analysis, violating purity. @@ -346,29 +346,39 @@ val is_annotated_* : t -> ... -> bool **Pattern**: Same as Task 3/4. **Changes**: -- [ ] Create `References` module with `builder` and `t` types -- [ ] `process_cmt_file` returns `References.builder` for both value and type refs -- [ ] `References.merge_all : builder list -> t` -- [ ] Delete global `ValueReferences.table` and `TypeReferences.table` +- [x] Create `References` module with `builder` and `t` types +- [x] Thread `~refs:References.builder` through `addValueReference`, `addTypeReference` +- [x] `process_cmt_file` returns `References.builder` in `file_data` +- [x] Merge refs into builder, process delayed items, then freeze +- [x] Solver uses `References.t` via `find_value_refs` and `find_type_refs` +- [x] Delete global `ValueReferences.table` and `TypeReferences.table` + +**Status**: Complete ✅ **Test**: Process files in different orders - results should be identical. **Estimated effort**: Medium (similar to Task 4) -### Task 6: Delayed items use map → list → merge pattern (P3) +### Task 6: Cross-file items use map → list → merge pattern (P3) -**Value**: No global queues. Delayed items are per-file immutable data. +**Value**: No global queues. Cross-file items are per-file immutable data. **Pattern**: Same as Task 3/4/5. **Changes**: -- [ ] Create `DelayedItems` module with `builder` and `t` types -- [ ] `process_cmt_file` returns `DelayedItems.builder` -- [ ] `DelayedItems.merge_all : builder list -> t` -- [ ] `forceDelayedItems` is pure function on `DelayedItems.t` -- [ ] Delete global `delayedItems` refs +- [x] Create `CrossFileItems` module with `builder` and `t` types +- [x] Thread `~cross_file:CrossFileItems.builder` through AST processing +- [x] `process_cmt_file` returns `CrossFileItems.builder` in `file_data` +- [x] `CrossFileItems.merge_all : builder list -> t` +- [x] `process_exception_refs` and `process_optional_args` are pure functions on merged `t` +- [x] Delete global `delayedItems` refs from `DeadException` and `DeadOptionalArgs` + +**Status**: Complete ✅ -**Key insight**: "Delayed" items are just per-file data collected during AST processing. +**Note**: `DeadType.TypeDependencies` was already per-file (processed within `process_cmt_file`), +so it didn't need to be included. + +**Key insight**: Cross-file items are references that span file boundaries. They should follow the same pattern as everything else. **Test**: Process files in different orders - results should be identical. @@ -496,6 +506,37 @@ This enables parallelization, caching, and incremental recomputation. --- +## Optional Future Tasks + +### Optional Task: Make OptionalArgs tracking immutable + +**Value**: Currently `CrossFileItems.process_optional_args` mutates `optionalArgs` inside declarations. +Making this immutable would complete the pure pipeline. + +**Current state**: +- `OptionalArgs.t` inside `decl.declKind = Value {optionalArgs}` is mutable +- `OptionalArgs.call` and `OptionalArgs.combine` mutate the record +- This happens after merge but before solver + +**Why it's acceptable now**: +- Mutation happens in a well-defined phase (after merge, before solver) +- Solver sees effectively immutable data +- Order independence is maintained (calls accumulate, order doesn't matter) + +**Changes needed**: +- [ ] Make `OptionalArgs.t` an immutable data structure +- [ ] Collect call info during AST processing as `OptionalArgCalls.builder` +- [ ] Return calls from `process_cmt_file` in `file_data` +- [ ] Merge all calls after file processing +- [ ] Build final `OptionalArgs` state from merged calls (pure) +- [ ] Store immutable `OptionalArgs` in declarations + +**Estimated effort**: Medium-High (touches core data structures) + +**Priority**: Low (current design works, just not fully pure) + +--- + ## Success Criteria After all tasks: diff --git a/analysis/reanalyze/src/CrossFileItems.ml b/analysis/reanalyze/src/CrossFileItems.ml new file mode 100644 index 0000000000..1fcf99aa71 --- /dev/null +++ b/analysis/reanalyze/src/CrossFileItems.ml @@ -0,0 +1,92 @@ +(** Cross-file items collected during AST processing. + + These are references that span file boundaries and need to be resolved + after all files are processed. *) + +open Common + +(** {2 Item types} *) + +type exception_ref = {exception_path: Path.t; loc_from: Location.t} + +type optional_arg_call = { + pos_to: Lexing.position; + arg_names: string list; + arg_names_maybe: string list; +} + +type function_ref = {pos_from: Lexing.position; pos_to: Lexing.position} + +(** {2 Types} *) + +type t = { + exception_refs: exception_ref list; + optional_arg_calls: optional_arg_call list; + function_refs: function_ref list; +} + +type builder = { + mutable exception_refs: exception_ref list; + mutable optional_arg_calls: optional_arg_call list; + mutable function_refs: function_ref list; +} + +(** {2 Builder API} *) + +let create_builder () : builder = + {exception_refs = []; optional_arg_calls = []; function_refs = []} + +let add_exception_ref (b : builder) ~exception_path ~loc_from = + b.exception_refs <- {exception_path; loc_from} :: b.exception_refs + +let add_optional_arg_call (b : builder) ~pos_to ~arg_names ~arg_names_maybe = + b.optional_arg_calls <- + {pos_to; arg_names; arg_names_maybe} :: b.optional_arg_calls + +let add_function_reference (b : builder) ~pos_from ~pos_to = + b.function_refs <- {pos_from; pos_to} :: b.function_refs + +(** {2 Merge API} *) + +let merge_all (builders : builder list) : t = + let exception_refs = + builders |> List.concat_map (fun b -> b.exception_refs) + in + let optional_arg_calls = + builders |> List.concat_map (fun b -> b.optional_arg_calls) + in + let function_refs = builders |> List.concat_map (fun b -> b.function_refs) in + {exception_refs; optional_arg_calls; function_refs} + +(** {2 Processing API} *) + +let process_exception_refs (t : t) ~refs ~find_exception ~config = + t.exception_refs + |> List.iter (fun {exception_path; loc_from} -> + match find_exception exception_path with + | None -> () + | Some loc_to -> + DeadCommon.addValueReference ~config ~refs ~binding:Location.none + ~addFileReference:true ~locFrom:loc_from ~locTo:loc_to) + +let process_optional_args (t : t) ~decls = + (* Process optional arg calls *) + t.optional_arg_calls + |> List.iter (fun {pos_to; arg_names; arg_names_maybe} -> + match Declarations.find_opt decls pos_to with + | Some {declKind = Value r} -> + r.optionalArgs + |> OptionalArgs.call ~argNames:arg_names + ~argNamesMaybe:arg_names_maybe + | _ -> ()); + (* Process function references *) + t.function_refs + |> List.iter (fun {pos_from; pos_to} -> + match + ( Declarations.find_opt decls pos_from, + Declarations.find_opt decls pos_to ) + with + | Some {declKind = Value rFrom}, Some {declKind = Value rTo} + when not (OptionalArgs.isEmpty rTo.optionalArgs) -> + OptionalArgs.combine rFrom.optionalArgs rTo.optionalArgs + | _ -> ()) diff --git a/analysis/reanalyze/src/CrossFileItems.mli b/analysis/reanalyze/src/CrossFileItems.mli new file mode 100644 index 0000000000..23a15c7ff6 --- /dev/null +++ b/analysis/reanalyze/src/CrossFileItems.mli @@ -0,0 +1,52 @@ +(** Cross-file items collected during AST processing. + + These are references that span file boundaries and need to be resolved + after all files are processed. Two types are provided: + - [builder] - mutable, for AST processing + - [t] - immutable, for processing after merge *) + +(** {2 Types} *) + +type t +(** Immutable cross-file items - for processing after merge *) + +type builder +(** Mutable builder - for AST processing *) + +(** {2 Builder API - for AST processing} *) + +val create_builder : unit -> builder + +val add_exception_ref : + builder -> exception_path:Common.Path.t -> loc_from:Location.t -> unit +(** Add a cross-file exception reference (defined in another file). *) + +val add_optional_arg_call : + builder -> + pos_to:Lexing.position -> + arg_names:string list -> + arg_names_maybe:string list -> + unit +(** Add a cross-file optional argument call. *) + +val add_function_reference : + builder -> pos_from:Lexing.position -> pos_to:Lexing.position -> unit +(** Add a cross-file function reference (for optional args combining). *) + +(** {2 Merge API} *) + +val merge_all : builder list -> t +(** Merge all builders into one immutable result. Order doesn't matter. *) + +(** {2 Processing API - for after merge} *) + +val process_exception_refs : + t -> + refs:References.builder -> + find_exception:(Common.Path.t -> Location.t option) -> + config:DceConfig.t -> + unit +(** Process cross-file exception references. *) + +val process_optional_args : t -> decls:Declarations.t -> unit +(** Process cross-file optional argument calls and function references. *) diff --git a/analysis/reanalyze/src/DceFileProcessing.ml b/analysis/reanalyze/src/DceFileProcessing.ml index 3959508d6d..3d1b801305 100644 --- a/analysis/reanalyze/src/DceFileProcessing.ml +++ b/analysis/reanalyze/src/DceFileProcessing.ml @@ -40,6 +40,8 @@ let processSignature ~config ~decls ~(file : file_context) ~doValues ~doTypes type file_data = { annotations: FileAnnotations.builder; decls: Declarations.builder; + refs: References.builder; + cross_file: CrossFileItems.builder; } let process_cmt_file ~config ~(file : file_context) ~cmtFilePath @@ -55,6 +57,8 @@ let process_cmt_file ~config ~(file : file_context) ~cmtFilePath (* Mutable builders for AST processing *) let annotations = FileAnnotations.create_builder () in let decls = Declarations.create_builder () in + let refs = References.create_builder () in + let cross_file = CrossFileItems.create_builder () in (match cmt_infos.cmt_annots with | Interface signature -> CollectAnnotations.signature ~state:annotations ~config signature; @@ -69,11 +73,11 @@ let process_cmt_file ~config ~(file : file_context) ~cmtFilePath processSignature ~config ~decls ~file ~doValues:true ~doTypes:false structure.str_type; let doExternals = false in - DeadValue.processStructure ~config ~decls ~file:dead_common_file - ~doTypes:true ~doExternals + DeadValue.processStructure ~config ~decls ~refs ~cross_file + ~file:dead_common_file ~doTypes:true ~doExternals ~cmt_value_dependencies:cmt_infos.cmt_value_dependencies structure | _ -> ()); - DeadType.TypeDependencies.forceDelayedItems ~config; + DeadType.TypeDependencies.forceDelayedItems ~config ~refs; DeadType.TypeDependencies.clear (); (* Return builders - caller will merge and freeze *) - {annotations; decls} + {annotations; decls; refs; cross_file} diff --git a/analysis/reanalyze/src/DceFileProcessing.mli b/analysis/reanalyze/src/DceFileProcessing.mli index d5f152c5cd..8ced8500ca 100644 --- a/analysis/reanalyze/src/DceFileProcessing.mli +++ b/analysis/reanalyze/src/DceFileProcessing.mli @@ -14,8 +14,10 @@ type file_context = { type file_data = { annotations: FileAnnotations.builder; decls: Declarations.builder; + refs: References.builder; + cross_file: CrossFileItems.builder; } -(** Result of processing a cmt file - both annotations and declarations *) +(** Result of processing a cmt file - annotations, declarations, references, and delayed items *) val process_cmt_file : config:DceConfig.t -> diff --git a/analysis/reanalyze/src/DeadCommon.ml b/analysis/reanalyze/src/DeadCommon.ml index b212c55db7..216d564be6 100644 --- a/analysis/reanalyze/src/DeadCommon.ml +++ b/analysis/reanalyze/src/DeadCommon.ml @@ -60,13 +60,7 @@ type decls = decl PosHash.t (* NOTE: Global decls removed - now using Declarations.builder/t pattern *) -module ValueReferences = struct - (** all value references *) - let table = (PosHash.create 256 : PosSet.t PosHash.t) - - let add posTo posFrom = PosHash.addSet table posTo posFrom - let find pos = PosHash.findSet table pos -end +(* NOTE: Global ValueReferences removed - now using References.builder/t pattern *) (* Local reporting context used only while emitting dead-code warnings. It tracks, per file, the end position of the last value we reported on, @@ -79,13 +73,7 @@ module ReportingContext = struct let set_max_end (ctx : t) (pos : Lexing.position) = ctx := pos end -module TypeReferences = struct - (** all type references *) - let table = (PosHash.create 256 : PosSet.t PosHash.t) - - let add posTo posFrom = PosHash.addSet table posTo posFrom - let find pos = PosHash.findSet table pos -end +(* NOTE: Global TypeReferences removed - now using References.builder/t pattern *) let declGetLoc decl = let loc_start = @@ -99,7 +87,7 @@ let declGetLoc decl = in {Location.loc_start; loc_end = decl.posEnd; loc_ghost = false} -let addValueReference ~config ~(binding : Location.t) ~addFileReference +let addValueReference ~config ~refs ~(binding : Location.t) ~addFileReference ~(locFrom : Location.t) ~(locTo : Location.t) : unit = let effectiveFrom = if binding = Location.none then locFrom else binding in if not effectiveFrom.loc_ghost then ( @@ -107,7 +95,8 @@ let addValueReference ~config ~(binding : Location.t) ~addFileReference Log_.item "addValueReference %s --> %s@." (effectiveFrom.loc_start |> posToString) (locTo.loc_start |> posToString); - ValueReferences.add locTo.loc_start effectiveFrom.loc_start; + References.add_value_ref refs ~posTo:locTo.loc_start + ~posFrom:effectiveFrom.loc_start; if addFileReference && (not locTo.loc_ghost) && (not effectiveFrom.loc_ghost) @@ -349,7 +338,7 @@ module Decl = struct ReportingContext.set_max_end ctx decl.posEnd; insideReportedValue - let report ~config (ctx : ReportingContext.t) decl = + let report ~config ~refs (ctx : ReportingContext.t) decl = let insideReportedValue = decl |> isInsideReportedValue ctx in if decl.report then let name, message = @@ -382,7 +371,7 @@ module Decl = struct (WarningDeadType, "is a variant case which is never constructed") in let hasRefBelow () = - let refs = ValueReferences.find decl.pos in + let decl_refs = References.find_value_refs refs decl.pos in let refIsBelow (pos : Lexing.position) = decl.pos.pos_fname <> pos.pos_fname || decl.pos.pos_cnum < pos.pos_cnum @@ -390,7 +379,7 @@ module Decl = struct (* not a function defined inside a function, e.g. not a callback *) decl.posEnd.pos_cnum < pos.pos_cnum in - refs |> PosSet.exists refIsBelow + decl_refs |> References.PosSet.exists refIsBelow in let shouldEmitWarning = (not insideReportedValue) @@ -409,16 +398,16 @@ end let declIsDead ~annotations ~refs decl = let liveRefs = refs - |> PosSet.filter (fun p -> + |> References.PosSet.filter (fun p -> not (FileAnnotations.is_annotated_dead annotations p)) in - liveRefs |> PosSet.cardinal = 0 + liveRefs |> References.PosSet.cardinal = 0 && not (FileAnnotations.is_annotated_gentype_or_live annotations decl.pos) let doReportDead ~annotations pos = not (FileAnnotations.is_annotated_gentype_or_dead annotations pos) -let rec resolveRecursiveRefs ~annotations ~config ~decls +let rec resolveRecursiveRefs ~all_refs ~annotations ~config ~decls ~checkOptionalArg:(checkOptionalArgFn : config:DceConfig.t -> decl -> unit) ~deadDeclarations ~level ~orderedFiles ~refs ~refsBeingResolved decl : bool = @@ -445,7 +434,7 @@ let rec resolveRecursiveRefs ~annotations ~config ~decls let allDepsResolved = ref true in let newRefs = refs - |> PosSet.filter (fun pos -> + |> References.PosSet.filter (fun pos -> if pos = decl.pos then ( if Config.recursiveDebug then Log_.item "recursiveDebug %s ignoring reference to self@." @@ -461,12 +450,12 @@ let rec resolveRecursiveRefs ~annotations ~config ~decls | Some xDecl -> let xRefs = match xDecl.declKind |> DeclKind.isType with - | true -> TypeReferences.find pos - | false -> ValueReferences.find pos + | true -> References.find_type_refs all_refs pos + | false -> References.find_value_refs all_refs pos in let xDeclIsDead = xDecl - |> resolveRecursiveRefs ~annotations ~config ~decls + |> resolveRecursiveRefs ~all_refs ~annotations ~config ~decls ~checkOptionalArg:checkOptionalArgFn ~deadDeclarations ~level:(level + 1) ~orderedFiles ~refs:xRefs ~refsBeingResolved @@ -496,7 +485,7 @@ let rec resolveRecursiveRefs ~annotations ~config ~decls IncorrectDeadAnnotation); if config.DceConfig.cli.debug then let refsString = - newRefs |> PosSet.elements |> List.map posToString + newRefs |> References.PosSet.elements |> List.map posToString |> String.concat ", " in Log_.item "%s %s %s: %d references (%s) [%d]@." @@ -505,24 +494,24 @@ let rec resolveRecursiveRefs ~annotations ~config ~decls | false -> "Live") (decl.declKind |> DeclKind.toString) (decl.path |> Path.toString) - (newRefs |> PosSet.cardinal) + (newRefs |> References.PosSet.cardinal) refsString level); isDead -let reportDead ~annotations ~config ~decls +let reportDead ~annotations ~config ~decls ~refs ~checkOptionalArg: (checkOptionalArgFn : annotations:FileAnnotations.t -> config:DceConfig.t -> decl -> unit) = let iterDeclInOrder ~deadDeclarations ~orderedFiles decl = - let refs = + let decl_refs = match decl |> Decl.isValue with - | true -> ValueReferences.find decl.pos - | false -> TypeReferences.find decl.pos + | true -> References.find_value_refs refs decl.pos + | false -> References.find_type_refs refs decl.pos in - resolveRecursiveRefs ~annotations ~config ~decls + resolveRecursiveRefs ~all_refs:refs ~annotations ~config ~decls ~checkOptionalArg:(checkOptionalArgFn ~annotations) ~deadDeclarations ~level:0 ~orderedFiles - ~refsBeingResolved:(ref PosSet.empty) ~refs decl + ~refsBeingResolved:(ref PosSet.empty) ~refs:decl_refs decl |> ignore in if config.DceConfig.cli.debug then ( @@ -559,4 +548,4 @@ let reportDead ~annotations ~config ~decls !deadDeclarations |> List.fast_sort Decl.compareForReporting in let reporting_ctx = ReportingContext.create () in - sortedDeadDeclarations |> List.iter (Decl.report ~config reporting_ctx) + sortedDeadDeclarations |> List.iter (Decl.report ~config ~refs reporting_ctx) diff --git a/analysis/reanalyze/src/DeadException.ml b/analysis/reanalyze/src/DeadException.ml index 6cf1673359..7fc036b204 100644 --- a/analysis/reanalyze/src/DeadException.ml +++ b/analysis/reanalyze/src/DeadException.ml @@ -1,9 +1,6 @@ open DeadCommon open Common -type item = {exceptionPath: Path.t; locFrom: Location.t} - -let delayedItems = ref [] let declarations = Hashtbl.create 1 let add ~config ~decls ~file ~path ~loc ~(strLoc : Location.t) name = @@ -14,24 +11,17 @@ let add ~config ~decls ~file ~path ~loc ~(strLoc : Location.t) name = ~posStart:strLoc.loc_start ~declKind:Exception ~moduleLoc:(ModulePath.getCurrent ()).loc ~path ~loc -let forceDelayedItems ~config = - let items = !delayedItems |> List.rev in - delayedItems := []; - items - |> List.iter (fun {exceptionPath; locFrom} -> - match Hashtbl.find_opt declarations exceptionPath with - | None -> () - | Some locTo -> - (* Delayed exception references don't need a binding context; use an empty state. *) - addValueReference ~config ~binding:Location.none - ~addFileReference:true ~locFrom ~locTo) +let find_exception path = Hashtbl.find_opt declarations path -let markAsUsed ~config ~(binding : Location.t) ~(locFrom : Location.t) - ~(locTo : Location.t) path_ = +let markAsUsed ~config ~refs ~cross_file ~(binding : Location.t) + ~(locFrom : Location.t) ~(locTo : Location.t) path_ = if locTo.loc_ghost then (* Probably defined in another file, delay processing and check at the end *) let exceptionPath = path_ |> Path.fromPathT |> Path.moduleToImplementation in - delayedItems := {exceptionPath; locFrom} :: !delayedItems - else addValueReference ~config ~binding ~addFileReference:true ~locFrom ~locTo + CrossFileItems.add_exception_ref cross_file ~exception_path:exceptionPath + ~loc_from:locFrom + else + addValueReference ~config ~refs ~binding ~addFileReference:true ~locFrom + ~locTo diff --git a/analysis/reanalyze/src/DeadOptionalArgs.ml b/analysis/reanalyze/src/DeadOptionalArgs.ml index 9f5e4faf7c..282dfa93d9 100644 --- a/analysis/reanalyze/src/DeadOptionalArgs.ml +++ b/analysis/reanalyze/src/DeadOptionalArgs.ml @@ -3,16 +3,7 @@ open Common let active () = true -type item = { - posTo: Lexing.position; - argNames: string list; - argNamesMaybe: string list; -} - -let delayedItems = (ref [] : item list ref) -let functionReferences = (ref [] : (Lexing.position * Lexing.position) list ref) - -let addFunctionReference ~config ~decls ~(locFrom : Location.t) +let addFunctionReference ~config ~decls ~cross_file ~(locFrom : Location.t) ~(locTo : Location.t) = if active () then let posTo = locTo.loc_start in @@ -28,7 +19,8 @@ let addFunctionReference ~config ~decls ~(locFrom : Location.t) if config.DceConfig.cli.debug then Log_.item "OptionalArgs.addFunctionReference %s %s@." (posFrom |> posToString) (posTo |> posToString); - functionReferences := (posFrom, posTo) :: !functionReferences) + CrossFileItems.add_function_reference cross_file ~pos_from:posFrom + ~pos_to:posTo) let rec hasOptionalArgs (texpr : Types.type_expr) = match texpr.desc with @@ -48,12 +40,13 @@ let rec fromTypeExpr (texpr : Types.type_expr) = | Tsubst t -> fromTypeExpr t | _ -> [] -let addReferences ~config ~(locFrom : Location.t) ~(locTo : Location.t) ~path - (argNames, argNamesMaybe) = +let addReferences ~config ~cross_file ~(locFrom : Location.t) + ~(locTo : Location.t) ~path (argNames, argNamesMaybe) = if active () then ( let posTo = locTo.loc_start in let posFrom = locFrom.loc_start in - delayedItems := {posTo; argNames; argNamesMaybe} :: !delayedItems; + CrossFileItems.add_optional_arg_call cross_file ~pos_to:posTo + ~arg_names:argNames ~arg_names_maybe:argNamesMaybe; if config.DceConfig.cli.debug then Log_.item "DeadOptionalArgs.addReferences %s called with optional argNames:%s \ @@ -63,29 +56,6 @@ let addReferences ~config ~(locFrom : Location.t) ~(locTo : Location.t) ~path (argNamesMaybe |> String.concat ", ") (posFrom |> posToString)) -let forceDelayedItems ~decls = - let items = !delayedItems |> List.rev in - delayedItems := []; - items - |> List.iter (fun {posTo; argNames; argNamesMaybe} -> - match Declarations.find_opt decls posTo with - | Some {declKind = Value r} -> - r.optionalArgs |> OptionalArgs.call ~argNames ~argNamesMaybe - | _ -> ()); - let fRefs = !functionReferences |> List.rev in - functionReferences := []; - fRefs - |> List.iter (fun (posFrom, posTo) -> - match - ( Declarations.find_opt decls posFrom, - Declarations.find_opt decls posTo ) - with - | Some {declKind = Value rFrom}, Some {declKind = Value rTo} - when not (OptionalArgs.isEmpty rTo.optionalArgs) -> - (* Only process if target has optional args - matching original filtering *) - OptionalArgs.combine rFrom.optionalArgs rTo.optionalArgs - | _ -> ()) - let check ~annotations ~config:_ decl = match decl with | {declKind = Value {optionalArgs}} diff --git a/analysis/reanalyze/src/DeadType.ml b/analysis/reanalyze/src/DeadType.ml index aa401cfb15..5439041ed9 100644 --- a/analysis/reanalyze/src/DeadType.ml +++ b/analysis/reanalyze/src/DeadType.ml @@ -11,25 +11,25 @@ module TypeLabels = struct let find path = Hashtbl.find_opt table path end -let addTypeReference ~config ~posFrom ~posTo = +let addTypeReference ~config ~refs ~posFrom ~posTo = if config.DceConfig.cli.debug then Log_.item "addTypeReference %s --> %s@." (posFrom |> posToString) (posTo |> posToString); - TypeReferences.add posTo posFrom + References.add_type_ref refs ~posTo ~posFrom module TypeDependencies = struct let delayedItems = ref [] let add loc1 loc2 = delayedItems := (loc1, loc2) :: !delayedItems let clear () = delayedItems := [] - let processTypeDependency ~config + let processTypeDependency ~config ~refs ( ({loc_start = posTo; loc_ghost = ghost1} : Location.t), ({loc_start = posFrom; loc_ghost = ghost2} : Location.t) ) = if (not ghost1) && (not ghost2) && posTo <> posFrom then - addTypeReference ~config ~posTo ~posFrom + addTypeReference ~config ~refs ~posTo ~posFrom - let forceDelayedItems ~config = - List.iter (processTypeDependency ~config) !delayedItems + let forceDelayedItems ~config ~refs = + List.iter (processTypeDependency ~config ~refs) !delayedItems end let extendTypeDependencies ~config (loc1 : Location.t) (loc2 : Location.t) = diff --git a/analysis/reanalyze/src/DeadValue.ml b/analysis/reanalyze/src/DeadValue.ml index f7b0e2a9d6..caa7a04fd8 100644 --- a/analysis/reanalyze/src/DeadValue.ml +++ b/analysis/reanalyze/src/DeadValue.ml @@ -77,8 +77,8 @@ let collectValueBinding ~config ~decls ~file ~(current_binding : Location.t) in loc -let processOptionalArgs ~config ~expType ~(locFrom : Location.t) ~locTo ~path - args = +let processOptionalArgs ~config ~cross_file ~expType ~(locFrom : Location.t) + ~locTo ~path args = if expType |> DeadOptionalArgs.hasOptionalArgs then ( let supplied = ref [] in let suppliedMaybe = ref [] in @@ -107,10 +107,10 @@ let processOptionalArgs ~config ~expType ~(locFrom : Location.t) ~locTo ~path if argIsSupplied = None then suppliedMaybe := s :: !suppliedMaybe | _ -> ()); (!supplied, !suppliedMaybe) - |> DeadOptionalArgs.addReferences ~config ~locFrom ~locTo ~path) + |> DeadOptionalArgs.addReferences ~config ~cross_file ~locFrom ~locTo ~path) -let rec collectExpr ~config ~(last_binding : Location.t) super self - (e : Typedtree.expression) = +let rec collectExpr ~config ~refs ~cross_file ~(last_binding : Location.t) super + self (e : Typedtree.expression) = let locFrom = e.exp_loc in let binding = last_binding in (match e.exp_desc with @@ -123,9 +123,11 @@ let rec collectExpr ~config ~(last_binding : Location.t) super self Log_.item "addDummyReference %s --> %s@." (Location.none.loc_start |> Common.posToString) (locTo.loc_start |> Common.posToString); - ValueReferences.add locTo.loc_start Location.none.loc_start) + References.add_value_ref refs ~posTo:locTo.loc_start + ~posFrom:Location.none.loc_start) else - addValueReference ~config ~binding ~addFileReference:true ~locFrom ~locTo + addValueReference ~config ~refs ~binding ~addFileReference:true ~locFrom + ~locTo | Texp_apply { funct = @@ -138,7 +140,7 @@ let rec collectExpr ~config ~(last_binding : Location.t) super self args; } -> args - |> processOptionalArgs ~config ~expType:exp_type + |> processOptionalArgs ~config ~cross_file ~expType:exp_type ~locFrom:(locFrom : Location.t) ~locTo ~path | Texp_let @@ -179,23 +181,25 @@ let rec collectExpr ~config ~(last_binding : Location.t) super self && Ident.name etaArg = "eta" && Path.name idArg2 = "arg" -> args - |> processOptionalArgs ~config ~expType:exp_type + |> processOptionalArgs ~config ~cross_file ~expType:exp_type ~locFrom:(locFrom : Location.t) ~locTo ~path | Texp_field (_, _, {lbl_loc = {Location.loc_start = posTo; loc_ghost = false}; _}) -> if !Config.analyzeTypes then - DeadType.addTypeReference ~config ~posTo ~posFrom:locFrom.loc_start + DeadType.addTypeReference ~config ~refs ~posTo ~posFrom:locFrom.loc_start | Texp_construct ( _, {cstr_loc = {Location.loc_start = posTo; loc_ghost} as locTo; cstr_tag}, _ ) -> (match cstr_tag with | Cstr_extension path -> - path |> DeadException.markAsUsed ~config ~binding ~locFrom ~locTo + path + |> DeadException.markAsUsed ~config ~refs ~cross_file ~binding ~locFrom + ~locTo | _ -> ()); if !Config.analyzeTypes && not loc_ghost then - DeadType.addTypeReference ~config ~posTo ~posFrom:locFrom.loc_start + DeadType.addTypeReference ~config ~refs ~posTo ~posFrom:locFrom.loc_start | Texp_record {fields} -> fields |> Array.iter (fun (_, record_label_definition, _) -> @@ -204,7 +208,8 @@ let rec collectExpr ~config ~(last_binding : Location.t) super self -> (* Punned field in OCaml projects has ghost location in expression *) let e = {e with exp_loc = {exp_loc with loc_ghost = false}} in - collectExpr ~config ~last_binding super self e |> ignore + collectExpr ~config ~refs ~cross_file ~last_binding super self e + |> ignore | _ -> ()) | _ -> ()); super.Tast_mapper.expr self e @@ -219,7 +224,8 @@ let rec collectExpr ~config ~(last_binding : Location.t) super self With this annotation we declare a new type for each branch to allow the function to be typed. *) -let collectPattern ~config : _ -> _ -> Typedtree.pattern -> Typedtree.pattern = +let collectPattern ~config ~refs : + _ -> _ -> Typedtree.pattern -> Typedtree.pattern = fun super self pat -> let posFrom = pat.Typedtree.pat_loc.loc_start in (match pat.pat_desc with @@ -227,7 +233,7 @@ let collectPattern ~config : _ -> _ -> Typedtree.pattern -> Typedtree.pattern = cases |> List.iter (fun (_loc, {Types.lbl_loc = {loc_start = posTo}}, _pat, _) -> if !Config.analyzeTypes then - DeadType.addTypeReference ~config ~posFrom ~posTo) + DeadType.addTypeReference ~config ~refs ~posFrom ~posTo) | _ -> ()); super.Tast_mapper.pat self pat @@ -288,16 +294,18 @@ let rec processSignatureItem ~config ~decls ~file ~doTypes ~doValues ~moduleLoc ModulePath.setCurrent oldModulePath (* Traverse the AST *) -let traverseStructure ~config ~decls ~file ~doTypes ~doExternals - (structure : Typedtree.structure) : unit = +let traverseStructure ~config ~decls ~refs ~cross_file ~file ~doTypes + ~doExternals (structure : Typedtree.structure) : unit = let rec create_mapper (last_binding : Location.t) = let super = Tast_mapper.default in let rec mapper = { super with expr = - (fun _self e -> e |> collectExpr ~config ~last_binding super mapper); - pat = (fun _self p -> p |> collectPattern ~config super mapper); + (fun _self e -> + e + |> collectExpr ~config ~refs ~cross_file ~last_binding super mapper); + pat = (fun _self p -> p |> collectPattern ~config ~refs super mapper); structure_item = (fun _self (structureItem : Typedtree.structure_item) -> let oldModulePath = ModulePath.getCurrent () in @@ -400,7 +408,7 @@ let traverseStructure ~config ~decls ~file ~doTypes ~doExternals mapper.structure mapper structure |> ignore (* Merge a location's references to another one's *) -let processValueDependency ~config ~decls +let processValueDependency ~config ~decls ~refs ~cross_file ( ({ val_loc = {loc_start = {pos_fname = fnTo} as posTo; loc_ghost = ghost1} as @@ -415,12 +423,16 @@ let processValueDependency ~config ~decls Types.value_description) ) = if (not ghost1) && (not ghost2) && posTo <> posFrom then ( let addFileReference = fileIsImplementationOf fnTo fnFrom in - addValueReference ~config ~binding:Location.none ~addFileReference ~locFrom - ~locTo; - DeadOptionalArgs.addFunctionReference ~config ~decls ~locFrom ~locTo) + addValueReference ~config ~refs ~binding:Location.none ~addFileReference + ~locFrom ~locTo; + DeadOptionalArgs.addFunctionReference ~config ~decls ~cross_file ~locFrom + ~locTo) -let processStructure ~config ~decls ~file ~cmt_value_dependencies ~doTypes - ~doExternals (structure : Typedtree.structure) = - traverseStructure ~config ~decls ~file ~doTypes ~doExternals structure; +let processStructure ~config ~decls ~refs ~cross_file ~file + ~cmt_value_dependencies ~doTypes ~doExternals + (structure : Typedtree.structure) = + traverseStructure ~config ~decls ~refs ~cross_file ~file ~doTypes ~doExternals + structure; let valueDependencies = cmt_value_dependencies |> List.rev in - valueDependencies |> List.iter (processValueDependency ~config ~decls) + valueDependencies + |> List.iter (processValueDependency ~config ~decls ~refs ~cross_file) diff --git a/analysis/reanalyze/src/Reanalyze.ml b/analysis/reanalyze/src/Reanalyze.ml index 23ec4301aa..6cba973ac4 100644 --- a/analysis/reanalyze/src/Reanalyze.ml +++ b/analysis/reanalyze/src/Reanalyze.ml @@ -122,7 +122,6 @@ let runAnalysis ~dce_config ~cmtRoot = (* Map: process each file -> list of file_data *) let file_data_list = processCmtFiles ~config:dce_config ~cmtRoot in if dce_config.DceConfig.run.dce then ( - DeadException.forceDelayedItems ~config:dce_config; (* Merge: combine all builders -> immutable data *) let annotations = FileAnnotations.merge_all @@ -132,9 +131,24 @@ let runAnalysis ~dce_config ~cmtRoot = Declarations.merge_all (file_data_list |> List.map (fun fd -> fd.DceFileProcessing.decls)) in - (* Process delayed optional args with merged decls *) - DeadOptionalArgs.forceDelayedItems ~decls; - DeadCommon.reportDead ~annotations ~decls ~config:dce_config + let cross_file = + CrossFileItems.merge_all + (file_data_list |> List.map (fun fd -> fd.DceFileProcessing.cross_file)) + in + (* Merge refs into a single builder for delayed items processing *) + let refs_builder = References.create_builder () in + file_data_list + |> List.iter (fun fd -> + References.merge_into_builder ~from:fd.DceFileProcessing.refs + ~into:refs_builder); + (* Process cross-file exception refs - they write to refs_builder *) + CrossFileItems.process_exception_refs cross_file ~refs:refs_builder + ~find_exception:DeadException.find_exception ~config:dce_config; + (* Process cross-file optional args - they read decls *) + CrossFileItems.process_optional_args cross_file ~decls; + (* Now freeze refs for solver *) + let refs = References.freeze_builder refs_builder in + DeadCommon.reportDead ~annotations ~decls ~refs ~config:dce_config ~checkOptionalArg:DeadOptionalArgs.check; WriteDeadAnnotations.write ~config:dce_config); if dce_config.DceConfig.run.exception_ then diff --git a/analysis/reanalyze/src/References.ml b/analysis/reanalyze/src/References.ml new file mode 100644 index 0000000000..34f5017dea --- /dev/null +++ b/analysis/reanalyze/src/References.ml @@ -0,0 +1,75 @@ +(** References collected during dead code analysis. + + Two types are provided: + - [builder] - mutable, for AST processing + - [t] - immutable, for solver (read-only access) *) + +(* Position set - same definition as DeadCommon.PosSet *) +module PosSet = Set.Make (struct + type t = Lexing.position + + let compare = compare +end) + +(* Position-keyed hashtable *) +module PosHash = Hashtbl.Make (struct + type t = Lexing.position + + let hash x = + let s = Filename.basename x.Lexing.pos_fname in + Hashtbl.hash (x.Lexing.pos_cnum, s) + + let equal (x : t) y = x = y +end) + +(* Helper to add to a set in a hashtable *) +let addSet h k v = + let set = try PosHash.find h k with Not_found -> PosSet.empty in + PosHash.replace h k (PosSet.add v set) + +(* Helper to find a set in a hashtable *) +let findSet h k = try PosHash.find h k with Not_found -> PosSet.empty + +(* Internal representation: two hashtables *) +type refs_table = PosSet.t PosHash.t + +type builder = {value_refs: refs_table; type_refs: refs_table} + +type t = {value_refs: refs_table; type_refs: refs_table} + +(* ===== Builder API ===== *) + +let create_builder () : builder = + {value_refs = PosHash.create 256; type_refs = PosHash.create 256} + +let add_value_ref (builder : builder) ~posTo ~posFrom = + addSet builder.value_refs posTo posFrom + +let add_type_ref (builder : builder) ~posTo ~posFrom = + addSet builder.type_refs posTo posFrom + +let merge_into_builder ~(from : builder) ~(into : builder) = + PosHash.iter + (fun pos refs -> + refs |> PosSet.iter (fun fromPos -> addSet into.value_refs pos fromPos)) + from.value_refs; + PosHash.iter + (fun pos refs -> + refs |> PosSet.iter (fun fromPos -> addSet into.type_refs pos fromPos)) + from.type_refs + +let merge_all (builders : builder list) : t = + let result = create_builder () in + builders + |> List.iter (fun builder -> merge_into_builder ~from:builder ~into:result); + {value_refs = result.value_refs; type_refs = result.type_refs} + +let freeze_builder (builder : builder) : t = + (* Zero-copy freeze - builder should not be used after this *) + {value_refs = builder.value_refs; type_refs = builder.type_refs} + +(* ===== Read-only API ===== *) + +let find_value_refs (t : t) pos = findSet t.value_refs pos + +let find_type_refs (t : t) pos = findSet t.type_refs pos diff --git a/analysis/reanalyze/src/References.mli b/analysis/reanalyze/src/References.mli new file mode 100644 index 0000000000..977588dec2 --- /dev/null +++ b/analysis/reanalyze/src/References.mli @@ -0,0 +1,42 @@ +(** References collected during dead code analysis. + + Two types are provided: + - [builder] - mutable, for AST processing + - [t] - immutable, for solver (read-only access) + + References track which positions reference which declarations. + Both value references and type references are tracked. *) + +(** {2 Types} *) + +type t +(** Immutable references - for solver (read-only) *) + +type builder +(** Mutable builder - for AST processing *) + +(** {2 Builder API - for AST processing} *) + +val create_builder : unit -> builder +val add_value_ref : + builder -> posTo:Lexing.position -> posFrom:Lexing.position -> unit +val add_type_ref : + builder -> posTo:Lexing.position -> posFrom:Lexing.position -> unit + +val merge_into_builder : from:builder -> into:builder -> unit +(** Merge one builder into another. *) + +val merge_all : builder list -> t +(** Merge all builders into one immutable result. Order doesn't matter. *) + +val freeze_builder : builder -> t +(** Convert builder to immutable t. Builder should not be used after this. *) + +(** {2 Types for refs} *) + +module PosSet : Set.S with type elt = Lexing.position + +(** {2 Read-only API for t - for solver} *) + +val find_value_refs : t -> Lexing.position -> PosSet.t +val find_type_refs : t -> Lexing.position -> PosSet.t