You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TransparentCompiler: scripts read #load'ed files through the global FileSystem, and DocumentSource.Custom gives new versions on every call #20750
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.
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:
the #loaded files when GetProjectSnapshotFromScript is given DocumentSource.Custom (TransparentCompiler.fs#L2483);
every FSharpProjectOptions based call on a TransparentCompiler created with DocumentSource.Custom, through FSharpProjectSnapshot.FromOptions (FSharpProjectSnapshot.fs#L715-L738).
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?
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).
Activity
nojaf commentedon Oct 9, 2026
Two more things I found while looking into this. Links are to
mainat 409c7a6.4. The script closure reads
#loaded files through the global file systemClosureSourceOfFilenameopens every#loaded file withFileSystem.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#rinb.fsxis not seen untilb.fsxis saved. The testThe script load closure should always be evaluatedinstalls aFileSystemshim 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) andDocumentSource.Customis 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 changesThe key of the
ScriptClosurecache contains the text of the root script, the options and the stamp, but nothing of the loaded files (TransparentCompiler.fs#L524-L554).GetProjectSnapshotFromScriptcomputes a new closure every time and then callscaches.ScriptClosure.Getto populate the cache (TransparentCompiler.fs#L2462-L2475). When an entry with the same key and version exists,AsyncMemoize.Getreturns that entry and the new closure is discarded (AsyncMemoize.fs#L245-L260).ComputeTcConfigBuilderand 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#rtob.fsxanda.fsxis checked without it until the text ofa.fsxchanges. The test above passes becausec.fsxadds 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).
Use the checker's document source for script snapshots (#20751)