Skip to content

Commit 1b5dd46

Browse files
committed
DCE: Make OptionalArgs tracking immutable (Optional Task)
OptionalArgs.t is now fully immutable with no mutation of declarations. Key changes: - OptionalArgs.t: removed mutable fields - apply_call: pure function, returns new state - combine_pair: pure function, returns pair of new states - OptionalArgsState module in Common.ml for computed state map - compute_optional_args_state: returns immutable OptionalArgsState.t - DeadOptionalArgs.check: looks up state from map Architecture: - Declaration's optionalArgs = initial state (what args exist) - OptionalArgsState.t = computed state (after all calls/combines) - Solver uses OptionalArgsState.find_opt to get final state This completes the pure analysis pipeline - no mutation anywhere.
1 parent ccf17f8 commit 1b5dd46

File tree

7 files changed

+124
-65
lines changed

7 files changed

+124
-65
lines changed

analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -613,32 +613,23 @@ add `@dead` annotations.
613613

614614
## Optional Future Tasks
615615

616-
### Optional Task: Make OptionalArgs tracking immutable
616+
### Optional Task: Make OptionalArgs tracking immutable
617617

618-
**Value**: Currently `CrossFileItems.process_optional_args` mutates `optionalArgs` inside declarations.
619-
Making this immutable would complete the pure pipeline.
618+
**Value**: `OptionalArgs.t` is now fully immutable. No mutation of declarations.
620619

621-
**Current state**:
622-
- `OptionalArgs.t` inside `decl.declKind = Value {optionalArgs}` is mutable
623-
- `OptionalArgs.call` and `OptionalArgs.combine` mutate the record
624-
- This happens after merge but before solver
620+
**Changes made**:
621+
- [x] Made `OptionalArgs.t` immutable (no mutable fields)
622+
- [x] Added pure functions: `apply_call`, `combine_pair`
623+
- [x] Created `OptionalArgsState` module in `Common.ml` for state map
624+
- [x] `compute_optional_args_state` returns immutable state map
625+
- [x] `DeadOptionalArgs.check` looks up state from map
625626

626-
**Why it's acceptable now**:
627-
- Mutation happens in a well-defined phase (after merge, before solver)
628-
- Solver sees effectively immutable data
629-
- Order independence is maintained (calls accumulate, order doesn't matter)
627+
**Architecture**:
628+
- Declaration's `optionalArgs` = initial state (what args exist)
629+
- `OptionalArgsState.t` = computed state (after all calls/combines)
630+
- Solver uses `OptionalArgsState.find_opt` to get final state
630631

631-
**Changes needed**:
632-
- [ ] Make `OptionalArgs.t` an immutable data structure
633-
- [ ] Collect call info during AST processing as `OptionalArgCalls.builder`
634-
- [ ] Return calls from `process_cmt_file` in `file_data`
635-
- [ ] Merge all calls after file processing
636-
- [ ] Build final `OptionalArgs` state from merged calls (pure)
637-
- [ ] Store immutable `OptionalArgs` in declarations
638-
639-
**Estimated effort**: Medium-High (touches core data structures)
640-
641-
**Priority**: Low (current design works, just not fully pure)
632+
**Status**: Complete ✅
642633

643634
---
644635

analysis/reanalyze/src/Common.ml

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ module Path = struct
9999
end
100100

101101
module OptionalArgs = struct
102-
type t = {
103-
mutable count: int;
104-
mutable unused: StringSet.t;
105-
mutable alwaysUsed: StringSet.t;
106-
}
102+
type t = {count: int; unused: StringSet.t; alwaysUsed: StringSet.t}
103+
(** Immutable record tracking optional argument usage.
104+
- unused: args that have never been passed
105+
- alwaysUsed: args that are always passed (when count > 0)
106+
- count: number of calls observed *)
107107

108108
let empty =
109109
{unused = StringSet.empty; alwaysUsed = StringSet.empty; count = 0}
@@ -113,23 +113,27 @@ module OptionalArgs = struct
113113

114114
let isEmpty x = StringSet.is_empty x.unused
115115

116-
let call ~argNames ~argNamesMaybe x =
116+
(** Apply a call to the optional args state. Returns new state. *)
117+
let apply_call ~argNames ~argNamesMaybe x =
117118
let nameSet = argNames |> StringSet.of_list in
118119
let nameSetMaybe = argNamesMaybe |> StringSet.of_list in
119120
let nameSetAlways = StringSet.diff nameSet nameSetMaybe in
120-
if x.count = 0 then x.alwaysUsed <- nameSetAlways
121-
else x.alwaysUsed <- StringSet.inter nameSetAlways x.alwaysUsed;
122-
argNames
123-
|> List.iter (fun name -> x.unused <- StringSet.remove name x.unused);
124-
x.count <- x.count + 1
125-
126-
let combine x y =
121+
let alwaysUsed =
122+
if x.count = 0 then nameSetAlways
123+
else StringSet.inter nameSetAlways x.alwaysUsed
124+
in
125+
let unused =
126+
argNames
127+
|> List.fold_left (fun acc name -> StringSet.remove name acc) x.unused
128+
in
129+
{count = x.count + 1; unused; alwaysUsed}
130+
131+
(** Combine two optional args states (for function references).
132+
Returns a pair of updated states with intersected unused/alwaysUsed. *)
133+
let combine_pair x y =
127134
let unused = StringSet.inter x.unused y.unused in
128-
x.unused <- unused;
129-
y.unused <- unused;
130135
let alwaysUsed = StringSet.inter x.alwaysUsed y.alwaysUsed in
131-
x.alwaysUsed <- alwaysUsed;
132-
y.alwaysUsed <- alwaysUsed
136+
({x with unused; alwaysUsed}, {y with unused; alwaysUsed})
133137

134138
let iterUnused f x = StringSet.iter f x.unused
135139
let iterAlwaysUsed f x = StringSet.iter (fun s -> f s x.count) x.alwaysUsed
@@ -140,6 +144,29 @@ module OptionalArgs = struct
140144
StringSet.fold (fun s acc -> f s x.count acc) x.alwaysUsed init
141145
end
142146

147+
(* Position-keyed hashtable - shared across modules *)
148+
module PosHash = Hashtbl.Make (struct
149+
type t = Lexing.position
150+
151+
let hash x =
152+
let s = Filename.basename x.Lexing.pos_fname in
153+
Hashtbl.hash (x.Lexing.pos_cnum, s)
154+
155+
let equal (x : t) y = x = y
156+
end)
157+
158+
(** State map for computed OptionalArgs.
159+
Maps declaration position to final state after all calls/combines. *)
160+
module OptionalArgsState = struct
161+
type t = OptionalArgs.t PosHash.t
162+
163+
let create () : t = PosHash.create 256
164+
165+
let find_opt (state : t) pos = PosHash.find_opt state pos
166+
167+
let set (state : t) pos value = PosHash.replace state pos value
168+
end
169+
143170
module DeclKind = struct
144171
type t =
145172
| Exception

analysis/reanalyze/src/CrossFileItems.ml

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55

66
open Common
77

8+
(* Position-keyed hashtable *)
9+
module PosHash = Hashtbl.Make (struct
10+
type t = Lexing.position
11+
12+
let hash x =
13+
let s = Filename.basename x.Lexing.pos_fname in
14+
Hashtbl.hash (x.Lexing.pos_cnum, s)
15+
16+
let equal (x : t) y = x = y
17+
end)
18+
819
(** {2 Item types} *)
920

1021
type exception_ref = {exception_path: Path.t; loc_from: Location.t}
@@ -70,24 +81,39 @@ let process_exception_refs (t : t) ~refs ~file_deps ~find_exception ~config =
7081
~binding:Location.none ~addFileReference:true ~locFrom:loc_from
7182
~locTo:loc_to)
7283

73-
let process_optional_args (t : t) ~decls =
84+
(** Compute optional args state from calls and function references.
85+
Returns a map from position to final OptionalArgs.t state.
86+
Pure function - does not mutate declarations. *)
87+
let compute_optional_args_state (t : t) ~decls : OptionalArgsState.t =
88+
let state = OptionalArgsState.create () in
89+
(* Initialize state from declarations *)
90+
let get_state pos =
91+
match OptionalArgsState.find_opt state pos with
92+
| Some s -> s
93+
| None -> (
94+
match Declarations.find_opt decls pos with
95+
| Some {declKind = Value {optionalArgs}} -> optionalArgs
96+
| _ -> OptionalArgs.empty)
97+
in
98+
let set_state pos s = OptionalArgsState.set state pos s in
7499
(* Process optional arg calls *)
75100
t.optional_arg_calls
76101
|> List.iter (fun {pos_to; arg_names; arg_names_maybe} ->
77-
match Declarations.find_opt decls pos_to with
78-
| Some {declKind = Value r} ->
79-
r.optionalArgs
80-
|> OptionalArgs.call ~argNames:arg_names
81-
~argNamesMaybe:arg_names_maybe
82-
| _ -> ());
102+
let current = get_state pos_to in
103+
let updated =
104+
OptionalArgs.apply_call ~argNames:arg_names
105+
~argNamesMaybe:arg_names_maybe current
106+
in
107+
set_state pos_to updated);
83108
(* Process function references *)
84109
t.function_refs
85110
|> List.iter (fun {pos_from; pos_to} ->
86-
match
87-
( Declarations.find_opt decls pos_from,
88-
Declarations.find_opt decls pos_to )
89-
with
90-
| Some {declKind = Value rFrom}, Some {declKind = Value rTo}
91-
when not (OptionalArgs.isEmpty rTo.optionalArgs) ->
92-
OptionalArgs.combine rFrom.optionalArgs rTo.optionalArgs
93-
| _ -> ())
111+
let state_from = get_state pos_from in
112+
let state_to = get_state pos_to in
113+
if not (OptionalArgs.isEmpty state_to) then (
114+
let updated_from, updated_to =
115+
OptionalArgs.combine_pair state_from state_to
116+
in
117+
set_state pos_from updated_from;
118+
set_state pos_to updated_to));
119+
state

analysis/reanalyze/src/CrossFileItems.mli

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,9 @@ val process_exception_refs :
4949
unit
5050
(** Process cross-file exception references. *)
5151

52-
val process_optional_args : t -> decls:Declarations.t -> unit
53-
(** Process cross-file optional argument calls and function references. *)
52+
(** {2 Optional Args State} *)
53+
54+
val compute_optional_args_state :
55+
t -> decls:Declarations.t -> Common.OptionalArgsState.t
56+
(** Compute final optional args state from calls and function references.
57+
Pure function - does not mutate declarations. *)

analysis/reanalyze/src/DeadCommon.ml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,10 @@ let rec resolveRecursiveRefs ~all_refs ~annotations ~config ~decls
430430
refsString level);
431431
isDead
432432

433-
let reportDead ~annotations ~config ~decls ~refs ~file_deps
433+
let reportDead ~annotations ~config ~decls ~refs ~file_deps ~optional_args_state
434434
~checkOptionalArg:
435435
(checkOptionalArgFn :
436+
optional_args_state:OptionalArgsState.t ->
436437
annotations:FileAnnotations.t ->
437438
config:DceConfig.t ->
438439
decl ->
@@ -444,7 +445,7 @@ let reportDead ~annotations ~config ~decls ~refs ~file_deps
444445
| false -> References.find_type_refs refs decl.pos
445446
in
446447
resolveRecursiveRefs ~all_refs:refs ~annotations ~config ~decls
447-
~checkOptionalArg:(checkOptionalArgFn ~annotations)
448+
~checkOptionalArg:(checkOptionalArgFn ~optional_args_state ~annotations)
448449
~deadDeclarations ~issues ~level:0 ~orderedFiles
449450
~refsBeingResolved:(ref PosSet.empty) ~refs:decl_refs decl
450451
|> ignore

analysis/reanalyze/src/DeadOptionalArgs.ml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,21 @@ let addReferences ~config ~cross_file ~(locFrom : Location.t)
5656
(argNamesMaybe |> String.concat ", ")
5757
(posFrom |> posToString))
5858

59-
(** Check for optional args issues. Returns issues instead of logging. *)
60-
let check ~annotations ~config:_ decl : Common.issue list =
59+
(** Check for optional args issues. Returns issues instead of logging.
60+
Uses optional_args_state map for final computed state. *)
61+
let check ~optional_args_state ~annotations ~config:_ decl : Common.issue list =
6162
match decl with
6263
| {declKind = Value {optionalArgs}}
6364
when active ()
6465
&& not
6566
(FileAnnotations.is_annotated_gentype_or_live annotations decl.pos)
6667
->
68+
(* Look up computed state from map, fall back to declaration's initial state *)
69+
let state =
70+
match OptionalArgsState.find_opt optional_args_state decl.pos with
71+
| Some s -> s
72+
| None -> optionalArgs
73+
in
6774
let loc = decl |> declGetLoc in
6875
let unused_issues =
6976
OptionalArgs.foldUnused
@@ -87,7 +94,7 @@ let check ~annotations ~config:_ decl : Common.issue list =
8794
}
8895
in
8996
issue :: acc)
90-
optionalArgs []
97+
state []
9198
in
9299
let redundant_issues =
93100
OptionalArgs.foldAlwaysUsed
@@ -112,7 +119,7 @@ let check ~annotations ~config:_ decl : Common.issue list =
112119
}
113120
in
114121
issue :: acc)
115-
optionalArgs []
122+
state []
116123
in
117124
(* Reverse to maintain original order from iterUnused/iterAlwaysUsed *)
118125
List.rev unused_issues @ List.rev redundant_issues

analysis/reanalyze/src/Reanalyze.ml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,18 @@ let runAnalysis ~dce_config ~cmtRoot =
147147
CrossFileItems.process_exception_refs cross_file ~refs:refs_builder
148148
~file_deps:file_deps_builder ~find_exception:DeadException.find_exception
149149
~config:dce_config;
150-
(* Process cross-file optional args - they read decls *)
151-
CrossFileItems.process_optional_args cross_file ~decls;
150+
(* Compute optional args state (pure - no mutation) *)
151+
let optional_args_state =
152+
CrossFileItems.compute_optional_args_state cross_file ~decls
153+
in
152154
(* Now freeze refs and file_deps for solver *)
153155
let refs = References.freeze_builder refs_builder in
154156
let file_deps = FileDeps.freeze_builder file_deps_builder in
155157
(* Run the solver - returns immutable AnalysisResult.t *)
156158
let analysis_result =
157159
DeadCommon.reportDead ~annotations ~decls ~refs ~file_deps
158-
~config:dce_config ~checkOptionalArg:DeadOptionalArgs.check
160+
~optional_args_state ~config:dce_config
161+
~checkOptionalArg:DeadOptionalArgs.check
159162
in
160163
(* Report all issues *)
161164
AnalysisResult.get_issues analysis_result

0 commit comments

Comments
 (0)