Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

- Reanalyze: add reactive incremental analysis (`-reactive`, `-runs`, `-churn`) and Mermaid pipeline dumping (`-mermaid`). https://github.com/rescript-lang/rescript/pull/8092

- Reanalyze: add `reanalyze-server` (long-lived server) with transparent delegation for `rescript-tools reanalyze -json`. https://github.com/rescript-lang/rescript/pull/8127

#### :bug: Bug fix

#### :memo: Documentation
Expand Down
7 changes: 0 additions & 7 deletions analysis/bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,6 @@ let main () =
in
Printf.printf "\"%s\"" res
| [_; "diagnosticSyntax"; path] -> Commands.diagnosticSyntax ~path
| _ :: "reanalyze" :: _ ->
let len = Array.length Sys.argv in
for i = 1 to len - 2 do
Sys.argv.(i) <- Sys.argv.(i + 1)
done;
Sys.argv.(len - 1) <- "";
Reanalyze.cli ()
| [_; "references"; path; line; col] ->
Commands.references ~path
~pos:(int_of_string line, int_of_string col)
Expand Down
73 changes: 67 additions & 6 deletions analysis/reanalyze/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ Dead code analysis and other experimental analyses for ReScript.

```bash
# Run DCE analysis on current project (reads rescript.json)
rescript-editor-analysis reanalyze -config
rescript-tools reanalyze -config

# Run DCE analysis on specific CMT directory
rescript-editor-analysis reanalyze -dce-cmt path/to/lib/bs
rescript-tools reanalyze -dce-cmt path/to/lib/bs

# Run all analyses
rescript-editor-analysis reanalyze -all
rescript-tools reanalyze -all
```

## Performance Options
Expand All @@ -28,7 +28,7 @@ rescript-editor-analysis reanalyze -all
Cache processed file data and skip unchanged files on subsequent runs:

```bash
rescript-editor-analysis reanalyze -config -reactive
rescript-tools reanalyze -config -reactive
```

This provides significant speedup for repeated analysis (e.g., in a watch mode or service):
Expand All @@ -43,7 +43,7 @@ This provides significant speedup for repeated analysis (e.g., in a watch mode o
Run analysis multiple times to measure cache effectiveness:

```bash
rescript-editor-analysis reanalyze -config -reactive -timing -runs 3
rescript-tools reanalyze -config -reactive -timing -runs 3
```

## CLI Flags
Expand Down Expand Up @@ -85,7 +85,68 @@ The reactive mode (`-reactive`) caches processed per-file results and efficientl
2. **Subsequent runs**: Only changed files are re-processed
3. **Unchanged files**: Return cached `file_data` immediately (no I/O or unmarshalling)

This is the foundation for a persistent analysis service that can respond to file changes in milliseconds.
This is the foundation for the **reanalyze-server** — a persistent analysis service that keeps reactive state warm across requests.

## Reanalyze Server

A long-lived server process that keeps reactive analysis state warm across multiple requests. This enables fast incremental analysis for editor integration.

### Transparent Server Delegation

When a server is running on the default socket (`<projectRoot>/.rescript-reanalyze.sock`), the regular `reanalyze` command **automatically delegates** to it. This means:

1. **Start the server once** (in the background)
2. **Use the editor normally** — all `reanalyze` calls go through the server
3. **Enjoy fast incremental analysis** — typically 10x faster after the first run

This works transparently with the VS Code extension's "Start Code Analyzer" command.

### Quick Start

```bash
# From anywhere inside your project, start the server:
rescript-tools reanalyze-server

# Now any reanalyze call will automatically use the server:
rescript-tools reanalyze -json # → delegates to server
```

### Starting the Server

```bash
rescript-tools reanalyze-server [--socket <path>]
```

Options:
- `--socket <path>` — Unix domain socket path (default: `<projectRoot>/.rescript-reanalyze.sock`)

Examples:

```bash
# Start server with default socket (recommended)
rescript-tools reanalyze-server \

# With custom socket path
rescript-tools reanalyze-server \
--socket /tmp/my-custom.sock \
```

### Behavior

- **Transparent delegation**: Regular `reanalyze` calls automatically use the server if running
- **Default socket**: `<projectRoot>/.rescript-reanalyze.sock` (used by both server and client)
- **Socket location invariant**: socket is always in the project root; `reanalyze` may be called from anywhere inside the project
- **Reactive mode forced**: The server always runs with `-reactive` enabled internally
- **Same output**: stdout/stderr/exit-code match what a direct CLI invocation would produce
- **Incremental updates**: When source files change and the project is rebuilt, subsequent requests reflect the updated analysis

### Typical Workflow

1. **Start server** (once, in background)
2. **Edit source files**
3. **Rebuild project** (`yarn build` / `rescript build`)
4. **Use editor** — analysis requests automatically go through the server
5. **Stop server** when done (or leave running)

## Development

Expand Down
4 changes: 3 additions & 1 deletion analysis/reanalyze/src/EmitJson.ml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
let items = ref 0
let start () = Printf.printf "["
let start () =
items := 0;
Printf.printf "["
let finish () = Printf.printf "\n]\n"
let emitClose () = "\n}"

Expand Down
17 changes: 9 additions & 8 deletions analysis/reanalyze/src/Paths.ml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ let rec findProjectRoot ~dir =

let runConfig = RunConfig.runConfig

let setReScriptProjectRoot =
lazy
(runConfig.projectRoot <- findProjectRoot ~dir:(Sys.getcwd ());
runConfig.bsbProjectRoot <-
(match Sys.getenv_opt "BSB_PROJECT_ROOT" with
| None -> runConfig.projectRoot
| Some s -> s))
let setProjectRootFromCwd () =
runConfig.projectRoot <- findProjectRoot ~dir:(Sys.getcwd ());
runConfig.bsbProjectRoot <-
(match Sys.getenv_opt "BSB_PROJECT_ROOT" with
| None -> runConfig.projectRoot
| Some s -> s)

let setReScriptProjectRoot = lazy (setProjectRootFromCwd ())

module Config = struct
let readSuppress conf =
Expand Down Expand Up @@ -84,7 +85,7 @@ module Config = struct

(* Read the config from rescript.json and apply it to runConfig and suppress and unsuppress *)
let processBsconfig () =
Lazy.force setReScriptProjectRoot;
setProjectRootFromCwd ();
let rescriptFile = Filename.concat runConfig.projectRoot rescriptJson in
let bsconfigFile = Filename.concat runConfig.projectRoot bsconfig in

Expand Down
23 changes: 17 additions & 6 deletions analysis/reanalyze/src/ReactiveAnalysis.ml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ type all_files_result = {
type t = (Cmt_format.cmt_infos, cmt_file_result option) ReactiveFileCollection.t
(** The reactive collection type *)

type processing_stats = {
mutable total_files: int;
mutable processed: int;
mutable from_cache: int;
}
(** Stats from a process_files call *)

(** Process cmt_infos into a file result *)
let process_cmt_infos ~config ~cmtFilePath cmt_infos : cmt_file_result option =
let excludePath sourceFile =
Expand Down Expand Up @@ -75,8 +82,10 @@ let create ~config : t =

(** Process all files incrementally using ReactiveFileCollection.
First run processes all files. Subsequent runs only process changed files.
Uses batch processing to emit all changes as a single Batch delta. *)
let process_files ~(collection : t) ~config:_ cmtFilePaths : all_files_result =
Uses batch processing to emit all changes as a single Batch delta.
Returns (result, stats) where stats contains processing information. *)
let process_files ~(collection : t) ~config:_ cmtFilePaths :
all_files_result * processing_stats =
Timing.time_phase `FileLoading (fun () ->
let total_files = List.length cmtFilePaths in
let cached_before =
Expand All @@ -90,6 +99,7 @@ let process_files ~(collection : t) ~config:_ cmtFilePaths : all_files_result =
ReactiveFileCollection.process_files_batch collection cmtFilePaths
in
let from_cache = total_files - processed in
let stats = {total_files; processed; from_cache} in

if !Cli.timing then
Printf.eprintf
Expand All @@ -113,10 +123,11 @@ let process_files ~(collection : t) ~config:_ cmtFilePaths : all_files_result =
| None -> ())
collection;

{
dce_data_list = List.rev !dce_data_list;
exception_results = List.rev !exception_results;
})
( {
dce_data_list = List.rev !dce_data_list;
exception_results = List.rev !exception_results;
},
stats ))

(** Get collection length *)
let length (collection : t) = ReactiveFileCollection.length collection
Expand Down
40 changes: 32 additions & 8 deletions analysis/reanalyze/src/Reanalyze.ml
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ let processFilesSequential ~config (cmtFilePaths : string list) :
{dce_data_list = !dce_data_list; exception_results = !exception_results})

(** Process all cmt files and return results for DCE and Exception analysis.
Conceptually: map process_cmt_file over all files. *)
let processCmtFiles ~config ~cmtRoot ~reactive_collection ~skip_file :
Conceptually: map process_cmt_file over all files.
If file_stats is provided, it will be updated with processing statistics. *)
let processCmtFiles ~config ~cmtRoot ~reactive_collection ~skip_file
?(file_stats : ReactiveAnalysis.processing_stats option) () :
all_files_result =
let cmtFilePaths =
let all = collectCmtFilePaths ~cmtRoot in
Expand All @@ -158,9 +160,15 @@ let processCmtFiles ~config ~cmtRoot ~reactive_collection ~skip_file :
(* Reactive mode: use incremental processing that skips unchanged files *)
match reactive_collection with
| Some collection ->
let result =
let result, stats =
ReactiveAnalysis.process_files ~collection ~config cmtFilePaths
in
(match file_stats with
| Some fs ->
fs.total_files <- stats.total_files;
fs.processed <- stats.processed;
fs.from_cache <- stats.from_cache
| None -> ());
{
dce_data_list = result.dce_data_list;
exception_results = result.exception_results;
Expand All @@ -180,10 +188,11 @@ let shuffle_list lst =
Array.to_list arr

let runAnalysis ~dce_config ~cmtRoot ~reactive_collection ~reactive_merge
~reactive_liveness ~reactive_solver ~skip_file =
~reactive_liveness ~reactive_solver ~skip_file ?file_stats () =
(* Map: process each file -> list of file_data *)
let {dce_data_list; exception_results} =
processCmtFiles ~config:dce_config ~cmtRoot ~reactive_collection ~skip_file
?file_stats ()
in
(* Get exception results from reactive collection if available *)
let exception_results =
Expand Down Expand Up @@ -549,7 +558,7 @@ let runAnalysisAndReport ~cmtRoot =
else None
in
runAnalysis ~dce_config ~cmtRoot ~reactive_collection ~reactive_merge
~reactive_liveness ~reactive_solver ~skip_file;
~reactive_liveness ~reactive_solver ~skip_file ();
(* Report issue count with diff *)
let current_count = Log_.Stats.get_issue_count () in
if !Cli.churn > 0 then (
Expand Down Expand Up @@ -599,7 +608,7 @@ let runAnalysisAndReport ~cmtRoot =
removed_std);
if !Cli.json then EmitJson.finish ()

let cli () =
let parse_argv (argv : string array) : string option =
let analysisKindSet = ref false in
let cmtRootRef = ref None in
let usage = "reanalyze version " ^ Version.version in
Expand Down Expand Up @@ -722,12 +731,27 @@ let cli () =
("--version", Unit versionAndExit, "Show version information and exit");
]
in
Arg.parse speclist print_endline usage;
let current = ref 0 in
Arg.parse_argv ~current argv speclist print_endline usage;
if !analysisKindSet = false then setConfig ();
let cmtRoot = !cmtRootRef in
!cmtRootRef

(** Default socket location invariant:
- the socket lives in the project root
- reanalyze can be called from anywhere within the project
Project root detection reuses the same logic as reanalyze config discovery:
walk up from a directory until we find rescript.json or bsconfig.json. *)
let cli () =
let cmtRoot = parse_argv Sys.argv in
runAnalysisAndReport ~cmtRoot
[@@raises exit]

(* Re-export server module for external callers (e.g. tools/bin/main.ml).
This keeps the wrapped-library layering intact: Reanalyze depends on internal
modules, not the other way around. *)
module ReanalyzeServer = ReanalyzeServer

module RunConfig = RunConfig
module DceConfig = DceConfig
module Log_ = Log_
Loading
Loading