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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.200.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396))
* Prevent `InvalidProgramException` when runtime async awaits a `Task<T>` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726))
* Preserve constraint inference order when explicit generic arguments share a caller type parameter, avoiding spurious FS0001 errors with multiply implemented generic interfaces. ([PR #20491](https://github.com/dotnet/fsharp/pull/20491))
* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async<FSharpFileSnapshot>`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750), [PR #20751](https://github.com/dotnet/fsharp/pull/20751))
* Build parser diagnostic messages without runtime printf specialization or reflective function construction, so Native AOT hosts report indentation and invalid-character diagnostics instead of failing during formatting. Preserve `#r` reference tooltips while script project options remain alive, including after garbage collection or cache eviction. ([PR #20658](https://github.com/dotnet/fsharp/pull/20658))
* Preserve tupled lambda argument groups when reducing state-machine applications, avoiding invalid argument substitutions and runtime failures with inline formatters. ([Issue #20675](https://github.com/dotnet/fsharp/issues/20675), [PR #20700](https://github.com/dotnet/fsharp/pull/20700))
* Preserve SRTP extension scopes across source files and competing witnesses through imported inline calls. Compile generic extension forwarding, including quotations, in unoptimized builds. Preserve applicable built-in operator priority in imported inline quotations. Do not reuse SRTP specializations across different selected witnesses. ([Issue #20683](https://github.com/dotnet/fsharp/issues/20683), [Issue #20684](https://github.com/dotnet/fsharp/issues/20684), [Issue #20685](https://github.com/dotnet/fsharp/issues/20685), [PR #20717](https://github.com/dotnet/fsharp/pull/20717))
Expand Down
42 changes: 17 additions & 25 deletions src/Compiler/Service/FSharpProjectSnapshot.fs
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,21 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta
|> Task.FromResult
)

static member CreateFromDocumentSource(fileName: string, documentSource: DocumentSource) =

match documentSource with
| DocumentSource.Custom f ->
let version = DateTime.Now.Ticks.ToString()

FSharpFileSnapshot(
fileName,
version,
fun () ->
task {
match! f fileName |> Async.StartAsTask with
| Some source -> return SourceTextNew.ofISourceText source
| None -> return failwith $"Couldn't get source for file {fileName}"
}
)

| DocumentSource.FileSystem -> FSharpFileSnapshot.CreateFromFileSystem fileName
/// A custom source gets the file text now and uses its checksum as the version,
/// so a file that did not change keeps its version. When it returns None, the file is read from disk.
static member CreateFromDocumentSource(fileName: string, documentSource: DocumentSource) : Async<FSharpFileSnapshot> =
async {
match documentSource with
| DocumentSource.Custom f ->
match! f fileName with
| Some source ->
let source = SourceTextNew.ofISourceText source
let version = source.GetChecksum().ToBuilder().ToArray() |> Md5Hasher.toString
return FSharpFileSnapshot(fileName, version, fun () -> Task.FromResult source)
| None -> return FSharpFileSnapshot.CreateFromFileSystem fileName

| DocumentSource.FileSystem -> return FSharpFileSnapshot.CreateFromFileSystem fileName
}

member public _.FileName = FileName
member _.Version = Version
Expand Down Expand Up @@ -713,12 +710,7 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
}

static member FromOptions(options: FSharpProjectOptions, documentSource: DocumentSource) =
FSharpProjectSnapshot.FromOptions(
options,
fun _ fileName ->
FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource)
|> async.Return
)
FSharpProjectSnapshot.FromOptions(options, fun _ fileName -> FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource))

static member FromOptions
(options: FSharpProjectOptions, fileName: string, fileVersion: int, sourceText: ISourceText, documentSource: DocumentSource)
Expand All @@ -731,9 +723,9 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
$"{fileVersion}{sourceText.GetHashCode().ToString()}",
fun () -> Task.FromResult(SourceTextNew.ofISourceText sourceText)
)
|> async.Return
else
FSharpFileSnapshot.CreateFromDocumentSource(fName, documentSource)
|> async.Return

FSharpProjectSnapshot.FromOptions(options, getFileSnapshot)

Expand Down
9 changes: 5 additions & 4 deletions src/Compiler/Service/TransparentCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2374,7 +2374,7 @@ type internal TransparentCompiler
fileName,
SourceTextNew.ofISourceText sourceText,
caret,
DocumentSource.FileSystem,
documentSource,
previewEnabled,
loadedTimeStamp,
otherFlags,
Expand Down Expand Up @@ -2474,13 +2474,14 @@ type internal TransparentCompiler
// Populate the cache.
let! _ = caches.ScriptClosure.Get(loadClosureKey, async { return loadClosure })

let sourceFiles =
let! sourceFiles =
loadClosure.SourceFiles
|> List.map (fun (sf, _) ->
if sf = fileName then
currentSourceFile
async.Return currentSourceFile
else
FSharpFileSnapshot.CreateFromDocumentSource(sf, documentSource))
|> MultipleDiagnosticsLoggers.Parallel

let references =
loadClosure.References
Expand All @@ -2497,7 +2498,7 @@ type internal TransparentCompiler
fileName + ".fsproj",
None,
None,
sourceFiles,
List.ofArray sourceFiles,
references,
otherFlags,
List.empty,
Expand Down
7 changes: 6 additions & 1 deletion src/Compiler/Service/service.fs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,12 @@ type FSharpChecker
?userOpName: string
) =
let userOpName = defaultArg userOpName "Unknown"
let documentSource = defaultArg documentSource DocumentSource.FileSystem

let documentSource =
match documentSource, getSource with
| Some documentSource, _ -> documentSource
| None, Some getSource -> DocumentSource.Custom getSource
| None, None -> DocumentSource.FileSystem

backgroundCompiler.GetProjectSnapshotFromScript(
fileName,
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Service/service.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ type public FSharpChecker =
/// <param name="fileName">Used to differentiate between scripts, to consider each script a separate project. Also used in formatted error messages.</param>
/// <param name="source">The source for the file.</param>
/// <param name="caret">The editor location for the cursor if available.</param>
/// <param name="documentSource">DocumentSource to load any additional files.</param>
/// <param name="documentSource">DocumentSource to load any additional files. Defaults to the document source of the checker.</param>
/// <param name="previewEnabled">Is the preview compiler enabled.</param>
/// <param name="loadedTimeStamp">Indicates when the script was loaded into the editing environment,
/// so that an 'unload' and 'reload' action will cause the script to be considered as a new project,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,77 @@ module TestsMutatingFileSystem =
FileSystemAutoOpens.FileSystem <- currentFileSystem
}

/// Writes a.fsx, which loads b.fsx, to disk and returns both paths.
let private createScriptThatLoadsFile () =
let directory = TestFramework.createTemporaryDirectory ()
let a = Path.Combine(directory.FullName, "a.fsx")
let b = Path.Combine(directory.FullName, "b.fsx")
File.WriteAllText(a, "#load \"b.fsx\"")
File.WriteAllText(b, "let onDisk = 1")
a, b

let private getLoadedFile (checker: FSharpChecker) (documentSource: DocumentSource option) (script: string) (loadedFile: string) =
async {
let! snapshot, _ =
checker.GetProjectSnapshotFromScript(script, SourceTextNew.ofString (File.ReadAllText script), ?documentSource = documentSource)
return snapshot.SourceFiles |> List.find (fun file -> file.FileName = loadedFile)
}

let private getText (file: ProjectSnapshot.FSharpFileSnapshot) =
task {
let! source = file.GetSource()
return source.GetSubTextString(0, source.Length)
}

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``GetProjectSnapshotFromScript uses the document source of the checker`` useTransparentCompiler =
task {
let a, b = createScriptThatLoadsFile ()
let getSource fileName = async { return if fileName = b then Some(SourceText.ofString "let inEditor = 1") else None }
let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler, documentSource = DocumentSource.Custom getSource)

let! loadedFile = getLoadedFile checker None a b
let! text = getText loadedFile
Assert.Equal("let inEditor = 1", text)
}

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``A custom document source gives a file the same version until its text changes`` useTransparentCompiler =
task {
let a, b = createScriptThatLoadsFile ()
let mutable textOfB = "let inEditor = 1"
let getSource fileName = async { return if fileName = b then Some(SourceText.ofString textOfB) else None }
let documentSource = Some(DocumentSource.Custom getSource)
let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler)

let! first = getLoadedFile checker documentSource a b
let! second = getLoadedFile checker documentSource a b
Assert.Equal(first.Version, second.Version)

textOfB <- "let inEditor = 2"
let! third = getLoadedFile checker documentSource a b
Assert.NotEqual<string>(first.Version, third.Version)
}

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``A custom document source that returns None reads the file from disk`` useTransparentCompiler =
task {
let a, b = createScriptThatLoadsFile ()
let getSource (_: string) = async { return None }
let documentSource = Some(DocumentSource.Custom getSource)
let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler)

let! loadedFile = getLoadedFile checker documentSource a b
let! text = getText loadedFile
Assert.Equal("let onDisk = 1", text)
}

[<Fact>]
let ``Parsing without cache and without project snapshot`` () =
async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2452,10 +2452,10 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean Equals(
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean IsSignatureFile
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean get_IsSignatureFile()
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot Create(System.String, System.String, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,System.Threading.Tasks.Task`1[FSharp.Compiler.Text.ISourceTextNew]])
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromDocumentSource(System.String, FSharp.Compiler.CodeAnalysis.DocumentSource)
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromFileSystem(System.String)
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromString(System.String, System.String)
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Int32 GetHashCode()
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] CreateFromDocumentSource(System.String, FSharp.Compiler.CodeAnalysis.DocumentSource)
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String FileName
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String GetFileName()
FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String Version
Expand Down
Loading