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
130 changes: 130 additions & 0 deletions markdown-pages/blog/reactive-analysis.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
author: rescript-team
date: "2025-12-28"
title: "Real-Time Analysis is Coming to ReScript"
badge: roadmap
description: |
ReScript's static analyzer is going reactive. Dead code detection that updates instantly as you edit, powered by novel reactive combinators.
---

## Introduction

Imagine editing a ReScript file and seeing dead code warnings update almost immediately as you work. No waiting for a full re-analysis command. Just continuous feedback about which parts of your code are actually used.

This is what we're bringing to ReScript.

The static analyzer that powers dead code detection is being rebuilt on a **reactive foundation**. When you add a reference to a function, the "unused" warning vanishes quickly. When you remove the last use of a module, the dead code warning appears right away. The analysis stays in sync with your code, updating in real time.
Comment on lines +12 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth adding that it's "project wide dead code warnings". Other tools (eslint etc) has plenty of dead code warnings etc for the current file. But this is for the entire project, something that is notoriously bad/slow in most solutions.


## Why This Matters

Traditional static analyzers work in batch mode: gather all files, analyze everything, report results. This works fine when you run analysis as a CI check or a manual command. But it's not what you want when you're actively editing code.

Batch analysis has an awkward tradeoff:

- **Run it rarely** and feedback comes too late to be useful
- **Run it often** and you're constantly waiting

What developers actually want is _continuous_ feedback that keeps up with their typing speed. That's exactly what reactive analysis provides.

## The Reactive Approach

Instead of re-analyzing your entire project on every change, the reactive analyzer represents the analysis as a **computation graph**. Each piece of data—declarations, references, liveness information—flows through this graph. When a file changes, only the affected parts of the graph recompute.

The result: analysis that completes in milliseconds for typical edits, even in large codebases.

### A Concrete Update Flow

Suppose you have a call graph where `main` calls `helper`, and `helper` calls `format` and `validate`. In your edit, `validate` starts calling `old`.

With batch analysis, the analyzer scans everything again. With reactive analysis:

1. The edited `.cmt` updates — **FlatMap** extracts the new reference. The compiler already resolved it: the ref knows its target (`old`) and its source location (inside `validate`)
2. **Join** maps that source position to its containing declaration, producing a new graph edge: _validate → old_
3. **Fixpoint** propagates reachability from roots (`main`) along edges — `old` now becomes reachable and enters the live set
4. A final **Classify** step (a join of declarations with the live set) marks `old` as live — so its dead-code warning disappears

The rest of the project graph stays untouched.

### A Glimpse Under the Hood

Here is a simplified view of the reactive dead code pipeline:

![Simplified reactive dead code pipeline](/blog/reactive-analysis/reactive-pipeline-simple.svg)

The analysis pipeline is built from composable operators:

- **Sources** hold the raw data from your `.cmt` files
- **FlatMap** extracts declarations, references, and annotations from each file's data
- **Union** merges collections — for example, combining value references, type references, and exception references into a unified set
- **Join** maps data across collections — for example, mapping each reference to the declaration that contains it, or partitioning declarations into dead and live using the fixpoint result
- **Fixpoint** computes transitive reachability — starting from root declarations (entry points, annotated `@live`, or externally referenced), following edges to find everything that's live

That last one, **fixpoint**, is particularly interesting. Dead code detection needs to know which declarations are _reachable_ from your entry points. This is a classic graph traversal—but doing it incrementally is hard. When you add one edge to a graph, how do you efficiently update the set of reachable nodes without recomputing from scratch?

In the running example above, only one new edge (`validate → old`) is added, and fixpoint propagates liveness from that delta.

![Fixpoint: incremental reachability](/blog/reactive-analysis/fixpoint.svg)

The reactive fixpoint operator solves exactly this problem. It takes a set of roots and a set of edges, and maintains the full reachable set. In this example, once `old` becomes reachable through `validate`, fixpoint only propagates through neighbors affected by that new edge, without revisiting unrelated parts of the graph. This is what makes the analysis fast enough for real-time use.

### Glitch-Free by Design

There's a subtle correctness issue with reactive systems: **glitches**. If node A depends on both B and C, and both B and C update, node A might briefly see an inconsistent state—B's new value with C's old value, or vice versa.

For analysis, glitches mean incorrect results. A function might briefly appear dead because the reference to it hasn't propagated yet, even though it's actually used.

The reactive scheduler prevents this with a simple principle: **accumulate, then propagate**. All updates at each level of the graph are collected before any downstream nodes run. Nodes process in topological order, wave by wave. The analysis never sees partial updates.

## What's Shipping

The reactive analysis infrastructure is landing now. Here's what it enables:

### For Developers

- **Editor integration**: Available now in the latest extension, with reactive dead code updates during active development
- **Monorepo support**: The analyzer now works correctly in monorepo setups, running from the workspace root with proper binary lookup
- **Faster CI analysis**: Incremental runs are dramatically faster than full runs
- **Immediate feedback loop**: See the impact of your changes instantly

### For Tooling Authors

The reactive primitives are general-purpose. The same infrastructure that powers dead code detection can express other analyses:

- Dependency graph visualization
- Unused export detection
- Reference counting and hotspot identification
- Custom project-specific checks

## The Road Ahead

This is the beginning of a larger shift. The same reactive foundation will extend to other parts of the editor experience:

- **Type checking**: Incremental type feedback without waiting for builds
- **Navigation**: Jump-to-definition that stays accurate as files change
- **Refactoring**: Real-time previews of rename and move operations

The goal is an editor experience where the tooling _keeps up with you_—no waiting, no stale results, just continuous assistance.

## Try It Today

**Requirements:**

Reactive analysis is a newer capability of Editor Code Analysis and requires a newer extension version.

- ReScript compiler >= 12.1.0
- VSCode or Cursor with rescript-vscode extension >= 1.73.9 (pre-release)

1. Start the build watcher (accept the prompt when opening a ReScript file, or run **"ReScript: Start Build"**)
2. Run **"ReScript: Start Code Analyzer"**

Dead code warnings appear in your editor and update reactively while you develop (currently driven by save/build updates). For configuration options and usage details, see the [Dead Code Analysis guide](../docs/manual/editor-code-analysis.mdx).

## Acknowledgments

The reactive primitives are based on work by [SkipLabs](https://skiplabs.io). Their reactive collections library provides the foundation for glitch-free, incremental computation that makes real-time analysis possible.

---

We're excited to bring this to the ReScript community. Static analysis that runs continuously, in the background, without you having to think about it—that's the experience we're building toward.

Stay tuned for broader reactive tooling updates. And as always, we welcome feedback on [GitHub](https://github.com/rescript-lang/rescript) and on the [forum](https://forum.rescript-lang.org).
4 changes: 4 additions & 0 deletions markdown-pages/docs/manual/editor-code-analysis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ ReScript’s language design allows for accurate and efficient dead code analysi

- The “Problems” pane populates with dead code warnings and suggestions.

### Reactive Updates (New)

Reactive dead code updates are a newer enhancement of Editor Code Analysis and require ReScript VSCode extension v1.73.9 or higher (pre-release).

## Real-World Use Cases

### 1. **Unused Record Fields**
Expand Down
2 changes: 1 addition & 1 deletion markdown-pages/docs/manual/editor-plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Look - [Editor Code Analysis](./editor-code-analysis.mdx) for a more detailed gu

### Caveats

- Doesn't support cross-package dead code analysis in monorepos. Run it per package instead.
- For older extension versions, cross-package dead code analysis in monorepos may be limited.

## Editor features

Expand Down
20 changes: 20 additions & 0 deletions public/blog/reactive-analysis/fixpoint.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#e8f5e9', 'primaryTextColor': '#102a43', 'primaryBorderColor': '#4caf50', 'lineColor': '#486581'}}}%%
flowchart LR
A_main["<b>main</b><br/><small>root</small>"]
A_helper["helper"]
A_format["format"]
A_validate["validate"]
A_old["old"]

A_main -- "wave 1" --> A_helper
A_helper -- "wave 2" --> A_format
A_helper -- "wave 2" --> A_validate
A_validate -. "new edge: validate → old" .-> A_old

classDef rootNode fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#102a43
classDef liveNode fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#102a43
classDef newlyLive fill:#fff9c4,stroke:#f9a825,stroke-width:2.5px,color:#102a43

class A_main rootNode
class A_helper,A_format,A_validate liveNode
class A_old newlyLive
Loading