Skip to content

Commit 9105092

Browse files
committed
DCE: Make analysis phase return AnalysisResult.t (Task 8 + 8b)
This completes Tasks 8 and 8b of the DCE refactor plan, making the analysis phase fully pure - returning an immutable AnalysisResult.t with no inline logging. 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 - resolveRecursiveRefs: collects all issues (optional args, incorrect annotations) via ~issues ref instead of logging inline Architecture after this change: merged_view (immutable) │ ▼ reportDead (pure function, no logging) │ ▼ AnalysisResult.t (immutable) │ ▼ report (all side effects here)
1 parent d21b1fb commit 9105092

File tree

8 files changed

+347
-83
lines changed

8 files changed

+347
-83
lines changed

analysis/reanalyze/DEADCODE_REFACTOR_PLAN.md

Lines changed: 120 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,127 @@ 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.
438484

439-
**Test**: Run analysis twice on same input, verify identical results. Verify no side effects.
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 ✅
519+
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`).
523+
524+
**Note**: Optional args and incorrect annotation issues were logged inline
525+
during `resolveRecursiveRefs`. Fixed in Task 8b.
526+
527+
### Task 8b: Collect all issues in AnalysisResult.t (P5) ✅
528+
529+
**Value**: Complete the pure analysis phase - all issues returned in result, no inline logging.
530+
531+
**Problem**: `resolveRecursiveRefs` was logging two types of issues inline:
532+
1. Optional args issues (from `checkOptionalArgFn`)
533+
2. Incorrect `@dead` annotation issues
534+
535+
These bypassed `AnalysisResult.t` and were logged directly via `Log_.warning`.
536+
537+
**Changes**:
538+
- [x] Pass `~issues:(Common.issue list ref)` through `resolveRecursiveRefs`
539+
- [x] Collect optional args issues instead of logging inline
540+
- [x] Collect incorrect annotation issues instead of logging inline
541+
- [x] Add collected issues to `AnalysisResult.t` in `reportDead`
542+
- [x] Remove all `Log_.warning` calls from `resolveRecursiveRefs`
543+
544+
**Status**: Complete ✅
440545

441-
**Estimated effort**: Medium (many logging call sites, but mechanical)
546+
**Key guarantee**: No `Log_.warning` calls in `resolveRecursiveRefs`. All issues
547+
are collected in `AnalysisResult.t` and logged by the caller.
442548

443549
### Task 9: ~~Separate annotation computation from file writing (P5)~~ REMOVED
444550

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

0 commit comments

Comments
 (0)