Skip to content

Commit d2a4278

Browse files
committed
DCE: Make analysis phase return AnalysisResult.t (Task 8)
This completes Task 8 of the DCE refactor plan, making the analysis phase return an immutable AnalysisResult.t instead of logging directly. Key changes: - Add AnalysisResult module with immutable result type and issue constructors - makeDeadIssue: pure function to create dead code issues - Decl.report: returns issue list (includes dead module + dead value issues) - DeadOptionalArgs.check: returns issue list instead of logging - DeadModules.checkModuleDead: returns issue option instead of logging - reportDead: returns AnalysisResult.t, caller logs issues Architecture after this change: merged_view (immutable) │ ▼ reportDead (pure function) │ ▼ AnalysisResult.t (immutable) │ ▼ report (side effects here only) Note: Optional args and incorrect annotation issues are still logged inline during resolveRecursiveRefs. A future task could collect these as well.
1 parent d21b1fb commit d2a4278

File tree

8 files changed

+314
-77
lines changed

8 files changed

+314
-77
lines changed

analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -410,16 +410,7 @@ They should follow the same pattern as everything else.
410410
**Value**: Analysis phase works on immutable merged data, returns immutable results.
411411
Can be parallelized, memoized, reordered.
412412

413-
**Changes**:
414-
- [ ] `solve_deadness : config -> merged_view -> analysis_result` (pure)
415-
- [ ] Input `merged_view` is immutable (from Tasks 4-7)
416-
- [ ] Output `analysis_result` is immutable
417-
- [ ] `Decl.report`: Return `issue` instead of logging
418-
- [ ] Remove all `Log_.warning`, `Log_.item` calls from analysis path
419-
- [ ] Side effects (logging, JSON) only in final reporting phase
420-
- [ ] Make `DeadModules` state part of `analysis_result` (currently mutated during solver)
421-
422-
**Architecture**:
413+
**Architecture goal**:
423414
```
424415
merged_view (immutable)
425416
@@ -433,12 +424,105 @@ analysis_result (immutable)
433424
report (side effects here only)
434425
```
435426

436-
**Key guarantee**: After Tasks 4-7, the analysis phase has **no mutable state**.
437-
This enables parallelization, caching, and incremental recomputation.
427+
**Approach**: Break into small, behavior-preserving steps. Each step can be verified
428+
before moving to the next. The key is: change return type, then immediately log at
429+
the call site, so behavior stays identical.
430+
431+
---
432+
433+
#### Task 8.1: Create `AnalysisResult` module ✅
434+
435+
**Changes**:
436+
- [x] Create `AnalysisResult.ml/mli` with `type t = { issues: Common.issue list }`
437+
- [x] Add constructors: `empty`, `add_issue`, `get_issues`
438+
- [x] Add issue constructors: `make_dead_issue`, `make_dead_module_issue`
439+
440+
**Verify**: Build succeeds. No behavior change (module not used yet).
441+
442+
---
443+
444+
#### Task 8.2: Make `emitWarning` return issue (behavior preserving) ✅
445+
446+
**Changes**:
447+
- [x] Add `makeDeadIssue` (pure function)
448+
- [x] `emitWarning` uses `makeDeadIssue` internally (later removed)
449+
450+
**Verify**: `make test-analysis` passes with identical output.
451+
452+
---
453+
454+
#### Task 8.3: Make `Decl.report` return issue option (behavior preserving) ✅
455+
456+
**Changes**:
457+
- [x] Change `Decl.report` signature to return `issue option`
458+
- [x] Use `makeDeadIssue` internally
459+
- [x] At call site in `reportDead`, log returned issue
460+
461+
**Verify**: `make test-analysis` passes with identical output.
462+
463+
---
464+
465+
#### Task 8.4: Make `DeadOptionalArgs.check` return issues (behavior preserving) ✅
466+
467+
**Changes**:
468+
- [x] Add `foldUnused`, `foldAlwaysUsed` to `OptionalArgs` module
469+
- [x] Change `check` signature to return `issue list` instead of `unit`
470+
- [x] At call site in `resolveRecursiveRefs`, immediately log returned issues
471+
472+
**Verify**: `make test-analysis` passes with identical output.
473+
474+
---
475+
476+
#### Task 8.5: Collect incorrect annotation issues (behavior preserving) ✅
477+
478+
**Changes**:
479+
- [x] Use `makeDeadIssue` for incorrect `@dead` annotation issues
480+
- [x] Log immediately at call site
481+
- [x] Remove `emitWarning` function (no longer needed)
482+
483+
**Verify**: `make test-analysis` passes with identical output.
484+
485+
---
486+
487+
#### Task 8.6: Make `DeadModules.checkModuleDead` return issue (behavior preserving) ✅
488+
489+
**Changes**:
490+
- [x] Change `DeadModules.checkModuleDead` to return `issue option`
491+
- [x] At call sites, log returned issue immediately
492+
493+
**Verify**: `make test-analysis` passes with identical output.
494+
495+
---
496+
497+
#### Task 8.7: Collect all issues in `reportDead` (behavior preserving) ✅
498+
499+
**Changes**:
500+
- [x] Change `Decl.report` to return `issue list` (includes dead module issues)
501+
- [x] Use `List.concat_map` to collect all issues
502+
- [x] Log all issues at the end of `reportDead`
503+
504+
**Verify**: `make test-analysis` passes with identical output.
505+
506+
---
507+
508+
#### Task 8.8: Return `AnalysisResult.t` from `reportDead`
509+
510+
**Changes**:
511+
- [x] Change `reportDead` to return `AnalysisResult.t` instead of `unit`
512+
- [x] Move logging from `reportDead` to caller in `Reanalyze.ml`
513+
514+
**Verify**: `make test-analysis` passes with identical output.
515+
516+
---
517+
518+
**Status**: Complete ✅
438519

439-
**Test**: Run analysis twice on same input, verify identical results. Verify no side effects.
520+
**Key guarantee**: The analysis phase (`reportDead`) now returns an immutable
521+
`AnalysisResult.t` containing all dead code issues. Side effects (logging)
522+
only happen in the caller (`Reanalyze.runAnalysis`).
440523

441-
**Estimated effort**: Medium (many logging call sites, but mechanical)
524+
**Note**: Optional args and incorrect annotation issues are still logged inline
525+
during `resolveRecursiveRefs`. A future task could collect these as well.
442526

443527
### Task 9: ~~Separate annotation computation from file writing (P5)~~ REMOVED
444528

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
(** Analysis result - immutable output from the solver.
2+
3+
The solver returns this instead of logging directly.
4+
All side effects (logging, JSON output) happen in the reporting phase. *)
5+
6+
open Common
7+
8+
type t = {issues: issue list}
9+
(** Immutable analysis result *)
10+
11+
let empty = {issues = []}
12+
13+
let add_issue result issue = {issues = issue :: result.issues}
14+
15+
let add_issues result new_issues =
16+
{issues = List.rev_append new_issues result.issues}
17+
18+
let get_issues result = result.issues |> List.rev
19+
20+
let issue_count result = List.length result.issues
21+
22+
(** Create a dead code issue *)
23+
let make_dead_issue ~loc ~deadWarning ~path ~message =
24+
{
25+
name =
26+
(match deadWarning with
27+
| WarningDeadException -> "Warning Dead Exception"
28+
| WarningDeadType -> "Warning Dead Type"
29+
| WarningDeadValue -> "Warning Dead Value"
30+
| WarningDeadValueWithSideEffects ->
31+
"Warning Dead Value With Side Effects"
32+
| IncorrectDeadAnnotation -> "Incorrect Dead Annotation");
33+
severity = Warning;
34+
loc;
35+
description = DeadWarning {deadWarning; path; message};
36+
}
37+
38+
(** Create a dead module issue *)
39+
let make_dead_module_issue ~loc ~moduleName =
40+
{
41+
name = "Warning Dead Module";
42+
severity = Warning;
43+
loc;
44+
description =
45+
DeadModule
46+
{
47+
message =
48+
Format.asprintf "@{<info>%s@} %s"
49+
(moduleName |> Name.toInterface |> Name.toString)
50+
"is a dead module as all its items are dead.";
51+
};
52+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
(** Analysis result - immutable output from the solver.
2+
3+
The solver returns this instead of logging directly.
4+
All side effects (logging, JSON output) happen in the reporting phase. *)
5+
6+
open Common
7+
8+
type t
9+
(** Immutable analysis result *)
10+
11+
val empty : t
12+
(** Empty result with no issues *)
13+
14+
val add_issue : t -> issue -> t
15+
(** Add a single issue to the result *)
16+
17+
val add_issues : t -> issue list -> t
18+
(** Add multiple issues to the result *)
19+
20+
val get_issues : t -> issue list
21+
(** Get all issues in order they were added *)
22+
23+
val issue_count : t -> int
24+
(** Count of issues *)
25+
26+
(** {2 Issue constructors} *)
27+
28+
val make_dead_issue :
29+
loc:Location.t ->
30+
deadWarning:deadWarning ->
31+
path:string ->
32+
message:string ->
33+
issue
34+
(** Create a dead code warning issue *)
35+
36+
val make_dead_module_issue : loc:Location.t -> moduleName:Name.t -> issue
37+
(** Create a dead module warning issue *)

analysis/reanalyze/src/Common.ml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ module OptionalArgs = struct
133133

134134
let iterUnused f x = StringSet.iter f x.unused
135135
let iterAlwaysUsed f x = StringSet.iter (fun s -> f s x.count) x.alwaysUsed
136+
137+
let foldUnused f x init = StringSet.fold f x.unused init
138+
139+
let foldAlwaysUsed f x init =
140+
StringSet.fold (fun s acc -> f s x.count acc) x.alwaysUsed init
136141
end
137142

138143
module DeclKind = struct

analysis/reanalyze/src/DeadCommon.ml

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,12 @@ let addValueDeclaration ~config ~decls ~file ?(isToplevel = true)
159159
~declKind:(Value {isToplevel; optionalArgs; sideEffects})
160160
~loc ~moduleLoc ~path
161161

162-
let emitWarning ~config ~decl ~message deadWarning =
162+
(** Create a dead code issue. Pure - no side effects. *)
163+
let makeDeadIssue ~decl ~message deadWarning : Common.issue =
163164
let loc = decl |> declGetLoc in
164-
decl.path
165-
|> Path.toModuleName ~isType:(decl.declKind |> DeclKind.isType)
166-
|> DeadModules.checkModuleDead ~config ~fileName:decl.pos.pos_fname;
167-
Log_.warning ~loc
168-
(DeadWarning {deadWarning; path = Path.withoutHead decl.path; message})
165+
AnalysisResult.make_dead_issue ~loc ~deadWarning
166+
~path:(Path.withoutHead decl.path)
167+
~message
169168

170169
module Decl = struct
171170
let isValue decl =
@@ -250,10 +249,13 @@ module Decl = struct
250249
ReportingContext.set_max_end ctx decl.posEnd;
251250
insideReportedValue
252251

253-
let report ~config ~refs (ctx : ReportingContext.t) decl =
252+
(** Report a dead declaration. Returns list of issues (dead module first, then dead value).
253+
Caller is responsible for logging. *)
254+
let report ~config ~refs (ctx : ReportingContext.t) decl : Common.issue list =
254255
let insideReportedValue = decl |> isInsideReportedValue ctx in
255-
if decl.report then
256-
let name, message =
256+
if not decl.report then []
257+
else
258+
let deadWarning, message =
257259
match decl.declKind with
258260
| Exception ->
259261
(WarningDeadException, "is never raised or passed as value")
@@ -300,11 +302,18 @@ module Decl = struct
300302
| _ -> true)
301303
&& (config.DceConfig.run.transitive || not (hasRefBelow ()))
302304
in
303-
if shouldEmitWarning then (
304-
decl.path
305-
|> Path.toModuleName ~isType:(decl.declKind |> DeclKind.isType)
306-
|> DeadModules.checkModuleDead ~config ~fileName:decl.pos.pos_fname;
307-
emitWarning ~config ~decl ~message name)
305+
if shouldEmitWarning then
306+
let dead_module_issue =
307+
decl.path
308+
|> Path.toModuleName ~isType:(decl.declKind |> DeclKind.isType)
309+
|> DeadModules.checkModuleDead ~config ~fileName:decl.pos.pos_fname
310+
in
311+
let dead_value_issue = makeDeadIssue ~decl ~message deadWarning in
312+
(* Return in order: dead module first (if any), then dead value *)
313+
match dead_module_issue with
314+
| Some mi -> [mi; dead_value_issue]
315+
| None -> [dead_value_issue]
316+
else []
308317
end
309318

310319
let declIsDead ~annotations ~refs decl =
@@ -320,7 +329,8 @@ let doReportDead ~annotations pos =
320329
not (FileAnnotations.is_annotated_gentype_or_dead annotations pos)
321330

322331
let rec resolveRecursiveRefs ~all_refs ~annotations ~config ~decls
323-
~checkOptionalArg:(checkOptionalArgFn : config:DceConfig.t -> decl -> unit)
332+
~checkOptionalArg:
333+
(checkOptionalArgFn : config:DceConfig.t -> decl -> Common.issue list)
324334
~deadDeclarations ~level ~orderedFiles ~refs ~refsBeingResolved decl : bool
325335
=
326336
match decl.pos with
@@ -387,14 +397,24 @@ let rec resolveRecursiveRefs ~all_refs ~annotations ~config ~decls
387397
if not (doReportDead ~annotations decl.pos) then decl.report <- false;
388398
deadDeclarations := decl :: !deadDeclarations)
389399
else (
390-
checkOptionalArgFn ~config decl;
400+
checkOptionalArgFn ~config decl
401+
|> List.iter (fun (issue : Common.issue) ->
402+
Log_.warning ~loc:issue.loc issue.description);
391403
decl.path
392404
|> DeadModules.markLive ~config
393405
~isType:(decl.declKind |> DeclKind.isType)
394406
~loc:decl.moduleLoc;
395-
if FileAnnotations.is_annotated_dead annotations decl.pos then
396-
emitWarning ~config ~decl ~message:" is annotated @dead but is live"
397-
IncorrectDeadAnnotation);
407+
if FileAnnotations.is_annotated_dead annotations decl.pos then (
408+
let issue =
409+
makeDeadIssue ~decl ~message:" is annotated @dead but is live"
410+
IncorrectDeadAnnotation
411+
in
412+
decl.path
413+
|> Path.toModuleName ~isType:(decl.declKind |> DeclKind.isType)
414+
|> DeadModules.checkModuleDead ~config ~fileName:decl.pos.pos_fname
415+
|> Option.iter (fun (mod_issue : Common.issue) ->
416+
Log_.warning ~loc:mod_issue.loc mod_issue.description);
417+
Log_.warning ~loc:issue.loc issue.description));
398418
if config.DceConfig.cli.debug then
399419
let refsString =
400420
newRefs |> References.PosSet.elements |> List.map posToString
@@ -413,7 +433,10 @@ let rec resolveRecursiveRefs ~all_refs ~annotations ~config ~decls
413433
let reportDead ~annotations ~config ~decls ~refs ~file_deps
414434
~checkOptionalArg:
415435
(checkOptionalArgFn :
416-
annotations:FileAnnotations.t -> config:DceConfig.t -> decl -> unit) =
436+
annotations:FileAnnotations.t ->
437+
config:DceConfig.t ->
438+
decl ->
439+
Common.issue list) : AnalysisResult.t =
417440
let iterDeclInOrder ~deadDeclarations ~orderedFiles decl =
418441
let decl_refs =
419442
match decl |> Decl.isValue with
@@ -459,5 +482,12 @@ let reportDead ~annotations ~config ~decls ~refs ~file_deps
459482
let sortedDeadDeclarations =
460483
!deadDeclarations |> List.fast_sort Decl.compareForReporting
461484
in
485+
(* Collect issues from dead declarations *)
462486
let reporting_ctx = ReportingContext.create () in
463-
sortedDeadDeclarations |> List.iter (Decl.report ~config ~refs reporting_ctx)
487+
let issues =
488+
sortedDeadDeclarations
489+
|> List.concat_map (fun decl ->
490+
Decl.report ~config ~refs reporting_ctx decl)
491+
in
492+
(* Return result - caller is responsible for logging *)
493+
AnalysisResult.add_issues AnalysisResult.empty issues

analysis/reanalyze/src/DeadModules.ml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ let markLive ~config ~isType ~(loc : Location.t) path =
1919
| Some (false, loc) -> Hashtbl.replace table moduleName (true, loc)
2020
| Some (true, _) -> ()
2121

22-
let checkModuleDead ~config ~fileName:pos_fname moduleName =
23-
if active ~config then
22+
(** Check if a module is dead and return issue if so. Pure - no logging. *)
23+
let checkModuleDead ~config ~fileName:pos_fname moduleName : Common.issue option
24+
=
25+
if not (active ~config) then None
26+
else
2427
match Hashtbl.find_opt table moduleName with
2528
| Some (false, loc) ->
2629
Hashtbl.remove table moduleName;
@@ -33,12 +36,5 @@ let checkModuleDead ~config ~fileName:pos_fname moduleName =
3336
{Location.loc_start = pos; loc_end = pos; loc_ghost = false}
3437
else loc
3538
in
36-
Log_.warning ~loc
37-
(Common.DeadModule
38-
{
39-
message =
40-
Format.asprintf "@{<info>%s@} %s"
41-
(moduleName |> Name.toInterface |> Name.toString)
42-
"is a dead module as all its items are dead.";
43-
})
44-
| _ -> ()
39+
Some (AnalysisResult.make_dead_module_issue ~loc ~moduleName)
40+
| _ -> None

0 commit comments

Comments
 (0)