dotnet / dotnet/fsharp

FSharp.Editor: full project-options/snapshot invalidation on reference-assembly change instead of point-in-place metadata reference update (like Roslyn)

Open
#20,131 0 comments 1 reaction 0 assignees View on GitHub
Needs-Triage
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 11h
Merged PRs (30d)
131

Description

## Summary

`FSharp.Editor` now watches on-disk `-r:` reference assemblies via `IVsFileChangeEx` (see `FileChangeWatcher.fs`, `FileChangeWatcherHub`, and its consumers in `FSharpProjectOptionsManager.fs` and `WorkspaceExtensions.fs`) and invalidates caches reactively instead of polling `File.GetLastWriteTimeUtc` on every comparison.

However, when a watched reference assembly changes on disk, the current behavior is a **coarse, whole-project invalidation**:

- `FSharpProjectOptionsReactor` (`FSharpProjectOptionsManager.fs`): `cache.TryRemove(projectId)` drops the *entire* cached `(Project * FSharpParsingOptions * FSharpProjectOptions)` tuple for the project, forcing a full recomputation of project options from scratch on next request.
- Transparent-compiler snapshot path (`WorkspaceExtensions.fs`): `latestSnapshots.TryRemove(project.Id)` drops the *entire* cached `FSharpProjectSnapshot`, forcing `FSharpProjectSnapshot.FromOptions` to rebuild the whole snapshot (all file snapshots, not just the changed reference) on next request.

This is architecturally different from how Roslyn handles the equivalent scenario.

## How Roslyn does it

Roslyn's `VisualStudioMetadataReferenceManager` / `FileChangeWatcher` infrastructure (`src/VisualStudio/Core/Def/ProjectSystem/VisualStudioMetadataReferenceManager.cs` and related `FileWatchedReferenceFactory` types) watches individual metadata reference files (PE references) and, on change, invalidates only the cached `PortableExecutableReference`/`MetadataReference` snapshot for *that specific file* (keyed by path in a `ReferenceCountedDisposable` / weak-reference cache). Roslyn's immutable `Solution`/`Project`/`Compilation` snapshots are structured so that a single reference can be swapped:

- `Project.WithMetadataReferences(...)` (and the underlying `Compilation.ReplaceReference` / `Compilation.WithReferences`) only replaces the one `MetadataReference` whose backing file changed; the rest of the `Compilation` (syntax trees, other references, symbol caches for unrelated assemblies) is structurally shared and reused.
- Because `Compilation` is designed around incremental, reference-level substitution as a first-class operation, invalidating one reference does not force Roslyn to re-parse/re-bind the whole project — only re-resolve/re-bind against the one updated reference.

## Why we don't do the same today

`FSharpProjectOptions` and `FSharpProjectSnapshot` are immutable value-like structures without a supported "replace this one reference and keep everything else structurally shared" API. `FSharpChecker`/FCS's incremental checking model is keyed off the whole `FSharpProjectOptions`/`ProjectSnapshot` identity (`OtherOptions` array, `ReferencedProjects`, etc.), not per-reference substitution. Introducing point-in-place reference updates would require:

- A stable, addressable identity for each metadata/reference-assembly entry inside `FSharpProjectOptions`/`ProjectSnapshot` (today they're just `-r:` strings in `OtherOptions`, or `FSharpReferencedProject` values without independent cache keys).
- Incremental-checking support in FCS's `IncrementalBuilder`/`TransparentCompiler` for substituting a single reference without invalidating the whole project's type-checking state (bound signature data, checked file results, etc. that depend on the reference set as a whole).
- Care around cross-project referencing (`AreFSharpInMemoryCrossProjectReferencesEnabled`) where downstream projects' checked state also depends on the changed reference.

This is a change to the core compiler/FCS incremental-checking model, not something that can be done from the editor layer alone (`vsintegration/src/FSharp.Editor`).

## Expected performance win

For solutions with many/large projects referencing a handful of frequently-rebuilt assemblies (e.g. multi-project solutions where a shared library is rebuilt in a loop during active development, or NuGet package caches refreshed on disk), whole-project invalidation means:

- Every keystroke-triggered background check after an external rebuild re-parses and re-type-checks the entire project (all source files), instead of just re-resolving symbols against the one updated assembly.
- For large projects (hundreds of files), this can mean seconds of unnecessary recomputation on every reference-assembly rebuild, compared to Roslyn's near-instantaneous reference swap for C#/VB projects of comparable size.
- This especially affects "F5 workflows" where a referenced class library is rebuilt frequently while iterating on a consuming F# project open in the IDE.

## Proposed follow-up

Track this as a separate, larger effort (likely spanning `FSharp.Compiler.Service`'s `IncrementalBuilder`/`TransparentCompiler` and the `FSharpProjectSnapshot`/`ProjectSnapshot` model) to support per-reference invalidation/substitution, so the editor-layer watcher introduced in `FileChangeWatcher.fs` can eventually trigger a targeted reference swap instead of a full project options/snapshot rebuild.

## Related

- `vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs`
- `vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs`
- `vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs`
- `src/Compiler/Service/FSharpProjectSnapshot.fs`

Contributor guide

Open the contributing guide

Research direction

Read FileChangeWatcher.fs, FSharpProjectOptionsManager.fs, WorkspaceExtensions.fs, and src/Compiler/Service/FSharpProjectSnapshot.fs, then trace how a reference-assembly change removes project caches. The work is done when FSharpProjectOptions/FSharpProjectSnapshot and the FCS incremental-checking path support targeted substitution of the changed reference without rebuilding the entire project, including cross-project references.

Written by the indexing model from the issue text.

Assessment

Tech stack
fsharp
Domain
compilers, devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.