Skip to content

TransparentCompiler: scripts read #load'ed files through the global FileSystem, and DocumentSource.Custom gives new versions on every call #20750

Description

@nojaf

FsAutoComplete installs a process-wide FileSystemAutoOpens.FileSystem shim so FCS reads the in-memory text of open files. We would like to stop doing that and give each checker its own view of the files (ionide/FsAutoComplete#1555). With the TransparentCompiler, project files already come from snapshots we build ourselves. Scripts are the part that still goes through the global file system, and the per-checker alternative, DocumentSource.Custom, does not work well there yet.

Links are to main at 409c7a6.

1. GetProjectSnapshotFromScript ignores the checker's document source

FSharpChecker.GetProjectSnapshotFromScript falls back to DocumentSource.FileSystem when no documentSource is passed, also when the checker was created with DocumentSource.Custom (service.fs#L533). The TransparentCompiler's GetProjectOptionsFromScript always passes DocumentSource.FileSystem (TransparentCompiler.fs#L2377). So the files a script #loads are read through the global file system (FSharpProjectSnapshot.fs#L81-L89).

2. A custom document source gives every file snapshot a new version

FSharpFileSnapshot.CreateFromDocumentSource uses DateTime.Now.Ticks as the version (FSharpProjectSnapshot.fs#L95). Each call creates new versions for files that did not change, so the TransparentCompiler caches miss. This happens for:

3. None from a custom document source throws

When the function returns None, CreateFromDocumentSource fails with "Couldn't get source for file" (FSharpProjectSnapshot.fs#L104). The BackgroundCompiler reads the file from disk in that case (FSharpSource.fs#L69-L77). An editor only knows the text of its open files, so with the TransparentCompiler it has to read every other file itself.

Possible changes

  • Use the checker's document source in GetProjectSnapshotFromScript when none is passed.
  • Let a custom source return a version with the text, or use a stable version, for example the last write time for files read from disk.
  • Treat None as "read from the file system", as the BackgroundCompiler does.
  • Or, for the snapshot API: let the caller create the snapshots of the #loaded files (a string -> Async<FSharpFileSnapshot> callback), as it already does for project files.

documentSource on FSharpChecker.Create is marked experimental and "likely to be removed in the future" (service.fsi#L53). If that is still the plan, which way should an editor give the TransparentCompiler the text of open files that a script #loads?

Activity

  1. added this to the Backlog milestone on Oct 9, 2026
  2. nojaf commented on Oct 9, 2026

    @nojaf
    ContributorAuthor

    Two more things I found while looking into this. Links are to main at 409c7a6.

    4. The script closure reads #loaded files through the global file system

    ClosureSourceOfFilename opens every #loaded file with FileSystem.OpenFileForReadShim (ScriptClosure.fs#L244-L258), whatever document source is used. The closure decides the nested #loads, #rs and #nowarns of the loaded files. So even when the snapshot of a loaded file comes from a custom source, an unsaved #load "c.fsx" or #r in b.fsx is not seen until b.fsx is saved. The test The script load closure should always be evaluated installs a FileSystem shim for this reason (TransparentCompiler.fs#L1155-L1172).

    A fix needs a source hook in LoadClosure.ComputeClosureOfScriptText. The closure code is synchronous (fsi uses it too) and DocumentSource.Custom is async, so either the hook is synchronous and the TransparentCompiler blocks on the callback, or the closure computation becomes async.

    5. The cached closure is not updated when only a #loaded file changes

    The key of the ScriptClosure cache contains the text of the root script, the options and the stamp, but nothing of the loaded files (TransparentCompiler.fs#L524-L554). GetProjectSnapshotFromScript computes a new closure every time and then calls caches.ScriptClosure.Get to populate the cache (TransparentCompiler.fs#L2462-L2475). When an entry with the same key and version exists, AsyncMemoize.Get returns that entry and the new closure is discarded (AsyncMemoize.fs#L245-L260).

    ComputeTcConfigBuilder and the check of the script then use the old closure (TransparentCompiler.fs#L831-L857, TransparentCompiler.fs#L1690). The snapshot has the new list of source files, but the references come from the old closure. Add a #r to b.fsx and a.fsx is checked without it until the text of a.fsx changes. The test above passes because c.fsx adds no references.

    A possible fix is to add the versions of the other source files in the snapshot to the key, because the snapshot contains all the files of the closure. That needs stable versions for those files (point 2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions