Skip to content
Merged
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
86 changes: 78 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 Expand Up @@ -204,3 +205,72 @@ let readSourceDirs ~configSources =
Log_.item "Types for cross-references will not be found.\n");
dirs := readDirsFromConfig ~configSources);
!dirs

type cmt_scan_entry = {
build_root: string;
scan_dirs: string list;
also_scan_build_root: bool;
}
(** Read explicit `.cmt/.cmti` scan plan from `.sourcedirs.json`.

This is a v2 extension produced by `rewatch` to support monorepos without requiring
reanalyze-side package resolution.

The scan plan is a list of build roots (usually `<pkg>/lib/bs`) relative to the project root,
plus a list of subdirectories (relative to that build root) to scan for `.cmt/.cmti`.

If missing, returns the empty list and callers should fall back to legacy behavior. *)

let readCmtScan () =
let sourceDirsFile =
["lib"; "bs"; ".sourcedirs.json"]
|> List.fold_left Filename.concat runConfig.bsbProjectRoot
in
let entries = ref [] in
let read_entry (json : Ext_json_types.t) =
match json with
| Ext_json_types.Obj {map} -> (
let build_root =
match StringMap.find_opt map "build_root" with
| Some (Ext_json_types.Str {str}) -> Some str
| _ -> None
in
let scan_dirs =
match StringMap.find_opt map "scan_dirs" with
| Some (Ext_json_types.Arr {content = arr}) ->
arr |> Array.to_list
|> List.filter_map (fun x ->
match x with
| Ext_json_types.Str {str} -> Some str
| _ -> None)
| _ -> []
in
let also_scan_build_root =
match StringMap.find_opt map "also_scan_build_root" with
| Some (Ext_json_types.True _) -> true
| Some (Ext_json_types.False _) -> false
| _ -> false
in
match build_root with
| Some build_root ->
entries := {build_root; scan_dirs; also_scan_build_root} :: !entries
| None -> ())
| _ -> ()
in
let read_cmt_scan (json : Ext_json_types.t) =
match json with
| Ext_json_types.Obj {map} -> (
match StringMap.find_opt map "cmt_scan" with
| Some (Ext_json_types.Arr {content = arr}) ->
arr |> Array.iter read_entry
| _ -> ())
| _ -> ()
in
if sourceDirsFile |> Sys.file_exists then (
let jsonOpt = sourceDirsFile |> Ext_json_parse.parse_json_from_file in
match jsonOpt with
| exception _ -> []
| json ->
read_cmt_scan json;
!entries |> List.rev)
else []
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
Loading
Loading