diff --git a/markdown-pages/blog/reactive-analysis.mdx b/markdown-pages/blog/reactive-analysis.mdx new file mode 100644 index 000000000..3cff7c05f --- /dev/null +++ b/markdown-pages/blog/reactive-analysis.mdx @@ -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. + +## 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). diff --git a/markdown-pages/docs/manual/editor-code-analysis.mdx b/markdown-pages/docs/manual/editor-code-analysis.mdx index c4d18d059..9a1a5bca4 100644 --- a/markdown-pages/docs/manual/editor-code-analysis.mdx +++ b/markdown-pages/docs/manual/editor-code-analysis.mdx @@ -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** diff --git a/markdown-pages/docs/manual/editor-plugins.mdx b/markdown-pages/docs/manual/editor-plugins.mdx index d6593695d..97c5bb884 100644 --- a/markdown-pages/docs/manual/editor-plugins.mdx +++ b/markdown-pages/docs/manual/editor-plugins.mdx @@ -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 diff --git a/public/blog/reactive-analysis/fixpoint.mmd b/public/blog/reactive-analysis/fixpoint.mmd new file mode 100644 index 000000000..8387dcd08 --- /dev/null +++ b/public/blog/reactive-analysis/fixpoint.mmd @@ -0,0 +1,20 @@ +%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#e8f5e9', 'primaryTextColor': '#102a43', 'primaryBorderColor': '#4caf50', 'lineColor': '#486581'}}}%% +flowchart LR + A_main["main
root"] + 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 diff --git a/public/blog/reactive-analysis/fixpoint.svg b/public/blog/reactive-analysis/fixpoint.svg new file mode 100644 index 000000000..bdb3e9225 --- /dev/null +++ b/public/blog/reactive-analysis/fixpoint.svg @@ -0,0 +1 @@ +

wave 1

wave 2

wave 2

new edge: validate → old

main
root

helper

format

validate

old

\ No newline at end of file diff --git a/public/blog/reactive-analysis/reactive-pipeline-simple.mmd b/public/blog/reactive-analysis/reactive-pipeline-simple.mmd new file mode 100644 index 000000000..2a2db3299 --- /dev/null +++ b/public/blog/reactive-analysis/reactive-pipeline-simple.mmd @@ -0,0 +1,24 @@ +%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#eef6ff', 'primaryTextColor': '#102a43', 'primaryBorderColor': '#4a90d9', 'lineColor': '#486581', 'secondaryColor': '#fff1f1', 'tertiaryColor': '#fff5e6'}}}%% +flowchart LR + sources["Sources"] + flatmap["FlatMap"] + edges["Edges"] + roots["Roots"] + fixpoint["Fixpoint"] + classify["Classify"] + issues["Issues"] + + sources --> flatmap + flatmap -- "refs, decls" --> edges + flatmap -- "refs, decls, ann." --> roots + edges --> fixpoint + roots --> fixpoint + flatmap -. "decls" .-> classify + fixpoint -- "live" --> classify + classify -- "dead" --> issues + + classDef coreBox fill:#eef6ff,stroke:#4a90d9,stroke-width:2px,color:#102a43 + classDef solveBox fill:#fff1f1,stroke:#cc6666,stroke-width:2px,color:#102a43 + + class sources,flatmap,edges,roots coreBox + class fixpoint,classify,issues solveBox diff --git a/public/blog/reactive-analysis/reactive-pipeline-simple.svg b/public/blog/reactive-analysis/reactive-pipeline-simple.svg new file mode 100644 index 000000000..f66bd8104 --- /dev/null +++ b/public/blog/reactive-analysis/reactive-pipeline-simple.svg @@ -0,0 +1 @@ +

refs, decls

refs, decls, ann.

decls

live

dead

Sources

FlatMap

Edges

Roots

Fixpoint

Classify

Issues

\ No newline at end of file diff --git a/scripts/test-hrefs.mjs b/scripts/test-hrefs.mjs index 285fc920a..4cab81c03 100644 --- a/scripts/test-hrefs.mjs +++ b/scripts/test-hrefs.mjs @@ -26,8 +26,27 @@ for (const file of files) { const warningMessage = log.replace(file, ""); + // Skip warnings about files that exist in public/ (served at root by Vite) + const missingFileMatches = [ + ...warningMessage.matchAll(/`\.\.\/\.\.\/(.*?)`/g), + ]; + let allMissingExistInPublic = false; + if (missingFileMatches.length > 0) { + allMissingExistInPublic = ( + await Promise.all( + missingFileMatches.map(([, p]) => + fs.access("public/" + p).then( + () => true, + () => false, + ), + ), + ) + ).every(Boolean); + } + if ( log && + !allMissingExistInPublic && !warningMessage.includes("api/") && // When running on CI it fails to ignore the link directly to the blog root // https://github.com/rescript-lang/rescript-lang.org/actions/runs/19520461368/job/55882556586?pr=1115#step:6:338