From eeb49f46e230a7353a3b619859a63c26d3088581 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 17 Mar 2021 18:35:07 -0700 Subject: [PATCH 01/10] Added FSharpReferencedProject --- src/fsharp/CompilerImports.fs | 30 ++++++++++++ src/fsharp/CompilerImports.fsi | 7 +++ src/fsharp/service/FSharpCheckerResults.fs | 27 +++++++++-- src/fsharp/service/FSharpCheckerResults.fsi | 11 ++++- src/fsharp/service/service.fs | 48 ++++++++++++++----- tests/service/MultiProjectAnalysisTests.fs | 18 +++---- .../FSharpProjectOptionsManager.fs | 25 +++++++++- .../ProjectSitesAndFiles.fs | 2 +- .../tests/UnitTests/ProjectOptionsBuilder.fs | 2 +- 9 files changed, 138 insertions(+), 32 deletions(-) diff --git a/src/fsharp/CompilerImports.fs b/src/fsharp/CompilerImports.fs index 8ebf45e3162..3b4450fe819 100644 --- a/src/fsharp/CompilerImports.fs +++ b/src/fsharp/CompilerImports.fs @@ -725,6 +725,36 @@ type RawFSharpAssemblyDataBackedByFileOnDisk (ilModule: ILModuleDef, ilAssemblyR let attrs = GetCustomAttributesOfILModule ilModule List.exists (IsMatchingSignatureDataVersionAttr ilg (parseILVersion Internal.Utilities.FSharpEnvironment.FSharpBinaryMetadataFormatRevision)) attrs +[] +type RawILAssemblyData (ilModule: ILModuleDef, ilAssemblyRefs) = + + interface IRawFSharpAssemblyData with + + member _.GetAutoOpenAttributes _ = List.empty + + member _.GetInternalsVisibleToAttributes ilg = GetInternalsVisibleToAttributes ilg ilModule + + member _.TryGetILModuleDef() = Some ilModule + + member _.GetRawFSharpSignatureData(_, _, _) = List.empty + + member _.GetRawFSharpOptimizationData(_, _, _) = List.empty + + member _.GetRawTypeForwarders() = + match ilModule.Manifest with + | Some manifest -> manifest.ExportedTypes + | None -> mkILExportedTypes [] + + member _.ShortAssemblyName = GetNameOfILModule ilModule + + member _.ILScopeRef = MakeScopeRefForILModule ilModule + + member _.ILAssemblyRefs = ilAssemblyRefs + + member _.HasAnyFSharpSignatureDataAttribute = false + + member _.HasMatchingFSharpSignatureDataAttribute _ = false + //---------------------------------------------------------------------------- // TcImports //-------------------------------------------------------------------------- diff --git a/src/fsharp/CompilerImports.fsi b/src/fsharp/CompilerImports.fsi index 41687660eb4..329dbcf945f 100644 --- a/src/fsharp/CompilerImports.fsi +++ b/src/fsharp/CompilerImports.fsi @@ -128,6 +128,13 @@ type TcAssemblyResolutions = static member GetAssemblyResolutionInformation: ctok: CompilationThreadToken * tcConfig: TcConfig -> AssemblyResolution list * UnresolvedAssemblyReference list +[] +type RawILAssemblyData = + + new : ilModule: ILModuleDef * ilAssemblyRefs: ILAssemblyRef list -> RawILAssemblyData + + interface IRawFSharpAssemblyData + /// Represents a table of imported assemblies with their resolutions. /// Is a disposable object, but it is recommended not to explicitly call Dispose unless you absolutely know nothing will be using its contents after the disposal. /// Otherwise, simply allow the GC to collect this and it will properly call Dispose from the finalizer. diff --git a/src/fsharp/service/FSharpCheckerResults.fs b/src/fsharp/service/FSharpCheckerResults.fs index 3690154175d..9e5b30684f3 100644 --- a/src/fsharp/service/FSharpCheckerResults.fs +++ b/src/fsharp/service/FSharpCheckerResults.fs @@ -55,14 +55,25 @@ open FSharp.Compiler.AbstractIL.ILBinaryReader type FSharpUnresolvedReferencesSet = FSharpUnresolvedReferencesSet of UnresolvedAssemblyReference list +[] +type FSharpReferencedProject = + | FSharp of projectFileName: string * options: FSharpProjectOptions + | IL of projectFileName: string * stamp: DateTime * lazyData: Lazy + + static member CreateFSharp(projectFileName, options) = + FSharp(projectFileName, options) + + static member CreateIL(projectFileName, stamp, lazyData) = + IL(projectFileName, stamp, lazyData) + // NOTE: may be better just to move to optional arguments here -type FSharpProjectOptions = +and FSharpProjectOptions = { ProjectFileName: string ProjectId: string option SourceFiles: string[] OtherOptions: string[] - ReferencedProjects: (string * FSharpProjectOptions)[] + ReferencedProjects: FSharpReferencedProject[] IsIncompleteTypeCheckEnvironment : bool UseScriptResolutionRules : bool LoadTime : System.DateTime @@ -89,9 +100,15 @@ type FSharpProjectOptions = options1.UnresolvedReferences = options2.UnresolvedReferences && options1.OriginalLoadReferences = options2.OriginalLoadReferences && options1.ReferencedProjects.Length = options2.ReferencedProjects.Length && - Array.forall2 (fun (n1,a) (n2,b) -> - n1 = n2 && - FSharpProjectOptions.AreSameForChecking(a,b)) options1.ReferencedProjects options2.ReferencedProjects && + (options1.ReferencedProjects, options2.ReferencedProjects) + ||> Array.forall2 (fun r1 r2 -> + match r1, r2 with + | FSharpReferencedProject.FSharp(n1,a), FSharpReferencedProject.FSharp(n2,b) -> + n1 = n2 && FSharpProjectOptions.AreSameForChecking(a,b) + | FSharpReferencedProject.IL(n1, stamp1, _), FSharpReferencedProject.IL(n2, stamp2, _) -> + n1 = n2 && stamp1 = stamp2 + | _ -> + false) && options1.LoadTime = options2.LoadTime member po.ProjectDirectory = System.IO.Path.GetDirectoryName(po.ProjectFileName) diff --git a/src/fsharp/service/FSharpCheckerResults.fsi b/src/fsharp/service/FSharpCheckerResults.fsi index 93ab0cfcf05..cb0adda12f8 100644 --- a/src/fsharp/service/FSharpCheckerResults.fsi +++ b/src/fsharp/service/FSharpCheckerResults.fsi @@ -46,7 +46,7 @@ type public FSharpProjectOptions = /// The command line arguments for the other projects referenced by this project, indexed by the /// exact text used in the "-r:" reference in FSharpProjectOptions. - ReferencedProjects: (string * FSharpProjectOptions)[] + ReferencedProjects: FSharpReferencedProject[] /// When true, the typechecking environment is known a priori to be incomplete, for /// example when a .fs file is opened outside of a project. In this case, the number of error @@ -82,6 +82,15 @@ type public FSharpProjectOptions = /// Compute the project directory. member internal ProjectDirectory: string +and [] public FSharpReferencedProject = + internal + | FSharp of projectFileName: string * options: FSharpProjectOptions + | IL of projectFileName: string * stamp: DateTime * lazyData: Lazy + + static member CreateFSharp : projectFileName: string * options: FSharpProjectOptions -> FSharpReferencedProject + + static member CreateIL : projectFileName: string * stamp: DateTime * lazyData: Lazy -> FSharpReferencedProject + /// Represents the use of an F# symbol from F# source code [] type public FSharpSymbolUse = diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 30aab3c66bd..a52f76ea44c 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -242,24 +242,46 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC cancellable { Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CreateOneIncrementalBuilder", options.ProjectFileName) let projectReferences = - [ for (nm,opts) in options.ReferencedProjects do + [ for r in options.ReferencedProjects do // Don't use cross-project references for FSharp.Core, since various bits of code require a concrete FSharp.Core to exist on-disk. // The only solutions that have these cross-project references to FSharp.Core are VisualFSharp.sln and FSharp.sln. The only ramification // of this is that you need to build FSharp.Core to get intellisense in those projects. - if (try Path.GetFileNameWithoutExtension(nm) with _ -> "") <> GetFSharpCoreLibraryName() then - - yield - { new IProjectReference with - member x.EvaluateRawContents(ctok) = - cancellable { - Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "GetAssemblyData", nm) - return! self.GetAssemblyData(opts, ctok, userOpName + ".CheckReferencedProject("+nm+")") - } - member x.TryGetLogicalTimeStamp(cache) = - self.TryGetLogicalTimeStampForProject(cache, opts) - member x.FileName = nm } ] + match r with + | FSharpReferencedProject.FSharp(nm,opts) -> + if (try Path.GetFileNameWithoutExtension(nm) with _ -> "") <> GetFSharpCoreLibraryName() then + + yield + { new IProjectReference with + member x.EvaluateRawContents(ctok) = + cancellable { + Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "GetAssemblyData", nm) + return! self.GetAssemblyData(opts, ctok, userOpName + ".CheckReferencedProject("+nm+")") + } + member x.TryGetLogicalTimeStamp(cache) = + self.TryGetLogicalTimeStampForProject(cache, opts) + member x.FileName = nm } + + | FSharpReferencedProject.IL(nm,stamp,lazyData) -> + yield + { new IProjectReference with + member x.EvaluateRawContents(_) = + cancellable { + let ilReaderOptions: ILReaderOptions = + { + pdbDirPath = None + reduceMemoryUsage = ReduceMemoryFlag.Yes + metadataOnly = MetadataOnlyFlag.Yes + tryGetMetadataSnapshot = fun _ -> None + } + let ilReader = ILBinaryReader.OpenILModuleReaderFromBytes nm lazyData.Value ilReaderOptions + let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs + return RawILAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData |> Some + } + member x.TryGetLogicalTimeStamp(_) = stamp |> Some + member x.FileName = nm } + ] let loadClosure = scriptClosureCache.TryGet(AnyCallerThread, options) diff --git a/tests/service/MultiProjectAnalysisTests.fs b/tests/service/MultiProjectAnalysisTests.fs index 9b3ec2b9a47..b6d83193a04 100644 --- a/tests/service/MultiProjectAnalysisTests.fs +++ b/tests/service/MultiProjectAnalysisTests.fs @@ -124,8 +124,8 @@ let u = Case1 3 let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args) { options with OtherOptions = Array.append options.OtherOptions [| ("-r:" + Project1A.dllName); ("-r:" + Project1B.dllName) |] - ReferencedProjects = [| (Project1A.dllName, Project1A.options); - (Project1B.dllName, Project1B.options); |] } + ReferencedProjects = [| FSharpReferencedProject.CreateFSharp(Project1A.dllName, Project1A.options); + FSharpReferencedProject.CreateFSharp(Project1B.dllName, Project1B.options); |] } let cleanFileName a = if a = fileName1 then "file1" else "??" [] @@ -309,7 +309,7 @@ let p = (""" let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args) { options with OtherOptions = Array.append options.OtherOptions [| for p in projects -> ("-r:" + p.DllName) |] - ReferencedProjects = [| for p in projects -> (p.DllName, p.Options); |] } + ReferencedProjects = [| for p in projects -> FSharpReferencedProject.CreateFSharp(p.DllName, p.Options); |] } { ModuleName = "JointProject"; FileName=fileName; Options = options; DllName=dllName } let cleanFileName a = @@ -427,7 +427,7 @@ let z = Project1.x let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args) { options with OtherOptions = Array.append options.OtherOptions [| ("-r:" + MultiProjectDirty1.dllName) |] - ReferencedProjects = [| (MultiProjectDirty1.dllName, MultiProjectDirty1.getOptions()) |] } + ReferencedProjects = [| FSharpReferencedProject.CreateFSharp(MultiProjectDirty1.dllName, MultiProjectDirty1.getOptions()) |] } [] let ``Test multi project symbols should pick up changes in dependent projects`` () = @@ -638,7 +638,7 @@ let v = Project2A.C().InternalMember // access an internal symbol let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args) { options with OtherOptions = Array.append options.OtherOptions [| ("-r:" + Project2A.dllName); |] - ReferencedProjects = [| (Project2A.dllName, Project2A.options); |] } + ReferencedProjects = [| FSharpReferencedProject.CreateFSharp(Project2A.dllName, Project2A.options); |] } let cleanFileName a = if a = fileName1 then "file1" else "??" //Project2A.fileSource1 @@ -662,7 +662,7 @@ let v = Project2A.C().InternalMember // access an internal symbol let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args) { options with OtherOptions = Array.append options.OtherOptions [| ("-r:" + Project2A.dllName); |] - ReferencedProjects = [| (Project2A.dllName, Project2A.options); |] } + ReferencedProjects = [| FSharpReferencedProject.CreateFSharp(Project2A.dllName, Project2A.options); |] } let cleanFileName a = if a = fileName1 then "file1" else "??" [] @@ -755,7 +755,7 @@ let fizzBuzz = function let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args) { options with OtherOptions = Array.append options.OtherOptions [| ("-r:" + Project3A.dllName) |] - ReferencedProjects = [| (Project3A.dllName, Project3A.options) |] } + ReferencedProjects = [| FSharpReferencedProject.CreateFSharp(Project3A.dllName, Project3A.options) |] } let cleanFileName a = if a = fileName1 then "file1" else "??" [] @@ -846,7 +846,7 @@ let ``Type provider project references should not throw exceptions`` () = yield "-r:" + r yield "-r:" + __SOURCE_DIRECTORY__ + @"/data/TypeProviderLibrary/TypeProviderLibrary.dll"|]; ReferencedProjects = - [|(__SOURCE_DIRECTORY__ + @"/data/TypeProviderLibrary/TypeProviderLibrary.dll", + [|FSharpReferencedProject.CreateFSharp(__SOURCE_DIRECTORY__ + @"/data/TypeProviderLibrary/TypeProviderLibrary.dll", {ProjectFileName = __SOURCE_DIRECTORY__ + @"/data/TypeProviderLibrary/TypeProviderLibrary.fsproj"; ProjectId = None SourceFiles = [|__SOURCE_DIRECTORY__ + @"/data/TypeProviderLibrary/Library1.fs"|]; @@ -934,7 +934,7 @@ let ``Projects creating generated types should not utilize cross-project-referen yield "-r:" + r yield "-r:" + __SOURCE_DIRECTORY__ + @"/data/TypeProvidersBug/TypeProvidersBug/bin/Debug/TypeProvidersBug.dll"|]; ReferencedProjects = - [|(__SOURCE_DIRECTORY__ + @"/data/TypeProvidersBug/TypeProvidersBug/bin/Debug/TypeProvidersBug.dll", + [|FSharpReferencedProject.CreateFSharp(__SOURCE_DIRECTORY__ + @"/data/TypeProvidersBug/TypeProvidersBug/bin/Debug/TypeProvidersBug.dll", {ProjectFileName = __SOURCE_DIRECTORY__ + @"/data/TypeProvidersBug/TypeProvidersBug/TypeProvidersBug.fsproj"; ProjectId = None diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 64e11d78e12..62d48d07be3 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -64,7 +64,16 @@ module private FSharpProjectOptionsHelpers = let doesProjectIdDiffer = p1.ProjectId <> p2.ProjectId let p1 = oldProject.Solution.GetProject(p1.ProjectId) let p2 = newProject.Solution.GetProject(p2.ProjectId) - doesProjectIdDiffer || p1.Version <> p2.Version + doesProjectIdDiffer || + ( + // For F# projects, just check the version until we have a better in-memory model for them. + if p1.Language = LanguageNames.FSharp then + p1.Version <> p2.Version + else + let v1 = p1.GetDependentVersionAsync(ct).Result + let v2 = p2.GetDependentVersionAsync(ct).Result + v1 <> v2 + ) ) let isProjectInvalidated (oldProject: Project) (newProject: Project) (settings: EditorOptions) = @@ -169,7 +178,19 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor if referencedProject.Language = FSharpConstants.FSharpLanguageName then match! tryComputeOptions referencedProject with | None -> canBail <- true - | Some(_, projectOptions) -> referencedProjects.Add(referencedProject.OutputFilePath, projectOptions) + | Some(_, projectOptions) -> referencedProjects.Add(FSharpReferencedProject.CreateFSharp(referencedProject.OutputFilePath, projectOptions)) + else + let! ilComp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask + referencedProjects.Add( + FSharpReferencedProject.CreateIL( + referencedProject.OutputFilePath, + DateTime.UtcNow, + lazy + let ms = new MemoryStream() + let emitOptions = Emit.EmitOptions(metadataOnly = true) + ilComp.Emit(ms, options = emitOptions) |> ignore + ms.ToArray() // REVIEW: Not performant but works until we figure a better way to get the data. + )) if canBail then return None diff --git a/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs b/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs index 4cc077dedda..5200249efab 100644 --- a/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs +++ b/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs @@ -286,7 +286,7 @@ type internal ProjectSitesAndFiles() = match tryGetOptionsForReferencedProject projectFileName with | None -> getProjectOptionsForProjectSite (enableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, projectSiteProvider.GetProjectSite(), serviceProvider, projectFileName, useUniqueStamp) |> snd | Some options -> options - yield projectFileName, (outputPath, referencedProjectOptions) |] + yield projectFileName, FSharpReferencedProject.CreateFSharp(outputPath, referencedProjectOptions) |] and getProjectOptionsForProjectSite(enableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, projectSite, serviceProvider, fileName, useUniqueStamp) = let referencedProjectFileNames, referencedProjectOptions = diff --git a/vsintegration/tests/UnitTests/ProjectOptionsBuilder.fs b/vsintegration/tests/UnitTests/ProjectOptionsBuilder.fs index 96e9a37537e..847adf617b2 100644 --- a/vsintegration/tests/UnitTests/ProjectOptionsBuilder.fs +++ b/vsintegration/tests/UnitTests/ProjectOptionsBuilder.fs @@ -124,7 +124,7 @@ module internal ProjectOptionsBuilder = let otherOptions = Array.append projectOptions.Options.OtherOptions binaryRefs { projectOptions with Options = { projectOptions.Options with - ReferencedProjects = referenceList + ReferencedProjects = referenceList |> Array.map FSharpReferencedProject.CreateFSharp OtherOptions = otherOptions } }) From 8f7b35b8f65d60a7cd7bf92852bc2f25b143dd8c Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 11:16:43 -0700 Subject: [PATCH 02/10] Touching up the API --- src/fsharp/service/FSharpCheckerResults.fs | 28 ++++++++++++++++----- src/fsharp/service/FSharpCheckerResults.fsi | 10 ++++++-- src/fsharp/service/service.fs | 12 ++++----- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/fsharp/service/FSharpCheckerResults.fs b/src/fsharp/service/FSharpCheckerResults.fs index 90e18806ed8..baf08cb8826 100644 --- a/src/fsharp/service/FSharpCheckerResults.fs +++ b/src/fsharp/service/FSharpCheckerResults.fs @@ -48,6 +48,7 @@ open FSharp.Compiler.Text.Position open FSharp.Compiler.Text.Range open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps +open System.Reflection.PortableExecutable open Internal.Utilities open Internal.Utilities.Collections @@ -57,14 +58,29 @@ type FSharpUnresolvedReferencesSet = FSharpUnresolvedReferencesSet of Unresolved [] type FSharpReferencedProject = - | FSharp of projectFileName: string * options: FSharpProjectOptions - | IL of projectFileName: string * stamp: DateTime * lazyData: Lazy + | FSharpReference of projectFileName: string * options: FSharpProjectOptions + | ILReference of projectFileName: string * stamp: DateTime * lazyData: Lazy + + member this.IsFSharp = + match this with + | FSharpReference _ -> true + | _ -> false + + member this.IsIL = + match this with + | ILReference _ -> true + | _ -> false + + member this.ProjectFileName = + match this with + | FSharpReference(projectFileName=projectFileName) + | ILReference(projectFileName=projectFileName) -> projectFileName static member CreateFSharp(projectFileName, options) = - FSharp(projectFileName, options) + FSharpReference(projectFileName, options) static member CreateIL(projectFileName, stamp, lazyData) = - IL(projectFileName, stamp, lazyData) + ILReference(projectFileName, stamp, lazyData) // NOTE: may be better just to move to optional arguments here and FSharpProjectOptions = @@ -103,9 +119,9 @@ and FSharpProjectOptions = (options1.ReferencedProjects, options2.ReferencedProjects) ||> Array.forall2 (fun r1 r2 -> match r1, r2 with - | FSharpReferencedProject.FSharp(n1,a), FSharpReferencedProject.FSharp(n2,b) -> + | FSharpReferencedProject.FSharpReference(n1,a), FSharpReferencedProject.FSharpReference(n2,b) -> n1 = n2 && FSharpProjectOptions.AreSameForChecking(a,b) - | FSharpReferencedProject.IL(n1, stamp1, _), FSharpReferencedProject.IL(n2, stamp2, _) -> + | FSharpReferencedProject.ILReference(n1, stamp1, _), FSharpReferencedProject.ILReference(n2, stamp2, _) -> n1 = n2 && stamp1 = stamp2 | _ -> false) && diff --git a/src/fsharp/service/FSharpCheckerResults.fsi b/src/fsharp/service/FSharpCheckerResults.fsi index c65a515a2da..794706b719b 100644 --- a/src/fsharp/service/FSharpCheckerResults.fsi +++ b/src/fsharp/service/FSharpCheckerResults.fsi @@ -84,8 +84,14 @@ type public FSharpProjectOptions = and [] public FSharpReferencedProject = internal - | FSharp of projectFileName: string * options: FSharpProjectOptions - | IL of projectFileName: string * stamp: DateTime * lazyData: Lazy + | FSharpReference of projectFileName: string * options: FSharpProjectOptions + | ILReference of projectFileName: string * stamp: DateTime * lazyData: Lazy + + member IsFSharp : bool + + member IsIL : bool + + member ProjectFileName : string static member CreateFSharp : projectFileName: string * options: FSharpProjectOptions -> FSharpReferencedProject diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 3843217380a..01849e454dd 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -244,13 +244,13 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CreateOneIncrementalBuilder", options.ProjectFileName) let projectReferences = [ for r in options.ReferencedProjects do - - // Don't use cross-project references for FSharp.Core, since various bits of code require a concrete FSharp.Core to exist on-disk. - // The only solutions that have these cross-project references to FSharp.Core are VisualFSharp.sln and FSharp.sln. The only ramification - // of this is that you need to build FSharp.Core to get intellisense in those projects. match r with - | FSharpReferencedProject.FSharp(nm,opts) -> + | FSharpReferencedProject.FSharpReference(nm,opts) -> + // Don't use cross-project references for FSharp.Core, since various bits of code require a concrete FSharp.Core to exist on-disk. + // The only solutions that have these cross-project references to FSharp.Core are VisualFSharp.sln and FSharp.sln. The only ramification + // of this is that you need to build FSharp.Core to get intellisense in those projects. + if (try Path.GetFileNameWithoutExtension(nm) with _ -> "") <> GetFSharpCoreLibraryName() then yield @@ -264,7 +264,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC self.TryGetLogicalTimeStampForProject(cache, opts) member x.FileName = nm } - | FSharpReferencedProject.IL(nm,stamp,lazyData) -> + | FSharpReferencedProject.ILReference(nm,stamp,lazyData) -> yield { new IProjectReference with member x.EvaluateRawContents(_) = From 86005c76d17171c0698cd77508adae14734a0baf Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 12:57:22 -0700 Subject: [PATCH 03/10] Refactoring and using a Stream instead of a byte[] --- src/fsharp/CompilerImports.fs | 23 +++++++++++---- src/fsharp/CompilerImports.fsi | 4 +-- src/fsharp/absil/ilread.fs | 29 +++++++++++++++++++ src/fsharp/absil/ilread.fsi | 8 +++++ src/fsharp/service/FSharpCheckerResults.fs | 22 +++++++++----- src/fsharp/service/FSharpCheckerResults.fsi | 11 +++++-- src/fsharp/service/service.fs | 6 ++-- .../FSharpProjectOptionsManager.fs | 11 ++++--- 8 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/fsharp/CompilerImports.fs b/src/fsharp/CompilerImports.fs index 3b4450fe819..8ee516b595e 100644 --- a/src/fsharp/CompilerImports.fs +++ b/src/fsharp/CompilerImports.fs @@ -726,19 +726,26 @@ type RawFSharpAssemblyDataBackedByFileOnDisk (ilModule: ILModuleDef, ilAssemblyR List.exists (IsMatchingSignatureDataVersionAttr ilg (parseILVersion Internal.Utilities.FSharpEnvironment.FSharpBinaryMetadataFormatRevision)) attrs [] -type RawILAssemblyData (ilModule: ILModuleDef, ilAssemblyRefs) = +type RawFSharpAssemblyData (ilModule: ILModuleDef, ilAssemblyRefs) = interface IRawFSharpAssemblyData with - member _.GetAutoOpenAttributes _ = List.empty + member _.GetAutoOpenAttributes ilg = GetAutoOpenAttributes ilg ilModule member _.GetInternalsVisibleToAttributes ilg = GetInternalsVisibleToAttributes ilg ilModule member _.TryGetILModuleDef() = Some ilModule - member _.GetRawFSharpSignatureData(_, _, _) = List.empty + member _.GetRawFSharpSignatureData(_, _, _) = + let resources = ilModule.Resources.AsList + [ for iresource in resources do + if IsSignatureDataResource iresource then + let ccuName = GetSignatureDataResourceName iresource + yield (ccuName, fun () -> iresource.GetBytes()) ] - member _.GetRawFSharpOptimizationData(_, _, _) = List.empty + member _.GetRawFSharpOptimizationData(_, _, _) = + ilModule.Resources.AsList + |> List.choose (fun r -> if IsOptimizationDataResource r then Some(GetOptimizationDataResourceName r, (fun () -> r.GetBytes())) else None) member _.GetRawTypeForwarders() = match ilModule.Manifest with @@ -751,9 +758,13 @@ type RawILAssemblyData (ilModule: ILModuleDef, ilAssemblyRefs) = member _.ILAssemblyRefs = ilAssemblyRefs - member _.HasAnyFSharpSignatureDataAttribute = false + member _.HasAnyFSharpSignatureDataAttribute = + let attrs = GetCustomAttributesOfILModule ilModule + List.exists IsSignatureDataVersionAttr attrs - member _.HasMatchingFSharpSignatureDataAttribute _ = false + member _.HasMatchingFSharpSignatureDataAttribute ilg = + let attrs = GetCustomAttributesOfILModule ilModule + List.exists (IsMatchingSignatureDataVersionAttr ilg (parseILVersion Internal.Utilities.FSharpEnvironment.FSharpBinaryMetadataFormatRevision)) attrs //---------------------------------------------------------------------------- // TcImports diff --git a/src/fsharp/CompilerImports.fsi b/src/fsharp/CompilerImports.fsi index 329dbcf945f..5f6782057ab 100644 --- a/src/fsharp/CompilerImports.fsi +++ b/src/fsharp/CompilerImports.fsi @@ -129,9 +129,9 @@ type TcAssemblyResolutions = static member GetAssemblyResolutionInformation: ctok: CompilationThreadToken * tcConfig: TcConfig -> AssemblyResolution list * UnresolvedAssemblyReference list [] -type RawILAssemblyData = +type RawFSharpAssemblyData = - new : ilModule: ILModuleDef * ilAssemblyRefs: ILAssemblyRef list -> RawILAssemblyData + new : ilModule: ILModuleDef * ilAssemblyRefs: ILAssemblyRef list -> RawFSharpAssemblyData interface IRawFSharpAssemblyData diff --git a/src/fsharp/absil/ilread.fs b/src/fsharp/absil/ilread.fs index 5e18cdb1430..fa83ab0bf18 100644 --- a/src/fsharp/absil/ilread.fs +++ b/src/fsharp/absil/ilread.fs @@ -28,6 +28,8 @@ open FSharp.Compiler.ErrorLogger open FSharp.Compiler.IO open FSharp.Compiler.Text.Range open System.Reflection +open System.Reflection.PortableExecutable +open FSharp.NativeInterop #nowarn "9" @@ -131,6 +133,27 @@ type ByteFile(fileName: string, bytes: byte[]) = member _.FileName = fileName interface BinaryFile with override bf.GetView() = view + +type PEFile(fileName: string, peReader: PEReader) as this = + + // We store a weak byte memory reference so we do not constantly create a lot of byte memory objects. + // We could just have a single ByteMemory stored in the PEFile, but we need to dispose of the stream via the finalizer; we cannot have a cicular reference. + let mutable weakMemory = new WeakReference(Unchecked.defaultof<_>) + + member _.FileName = fileName + + override _.Finalize() = + peReader.Dispose() + + interface BinaryFile with + override _.GetView() = + match weakMemory.TryGetTarget() with + | true, m -> m.AsReadOnly() + | _ -> + let block = peReader.GetEntireImage() // it's ok to call this everytime we do GetView as it is cached in the PEReader. + let m = ByteMemory.FromUnsafePointer(block.Pointer |> NativePtr.toNativeInt, block.Length, this) + weakMemory <- WeakReference(m) + m.AsReadOnly() /// Same as ByteFile but holds the bytes weakly. The bytes will be re-read from the backing file when a view is requested. /// This is the default implementation used by F# Compiler Services when accessing "stable" binaries. It is not used @@ -3888,6 +3911,12 @@ let OpenILModuleReaderFromBytes fileName assemblyContents options = let ilModule, ilAssemblyRefs, pdb = openPE (fileName, pefile, options.pdbDirPath, (options.reduceMemoryUsage = ReduceMemoryFlag.Yes), true) new ILModuleReaderImpl(ilModule, ilAssemblyRefs, (fun () -> ClosePdbReader pdb)) :> ILModuleReader +let OpenILModuleReaderFromStream fileName (peStream: Stream) options = + let peReader = new System.Reflection.PortableExecutable.PEReader(peStream, PEStreamOptions.PrefetchEntireImage) + let pefile = PEFile(fileName, peReader) :> BinaryFile + let ilModule, ilAssemblyRefs, pdb = openPE (fileName, pefile, options.pdbDirPath, (options.reduceMemoryUsage = ReduceMemoryFlag.Yes), true) + new ILModuleReaderImpl(ilModule, ilAssemblyRefs, (fun () -> ClosePdbReader pdb)) :> ILModuleReader + let ClearAllILModuleReaderCache() = ilModuleReaderCache1.Clear(ILModuleReaderCache1LockToken()) ilModuleReaderCache2.Clear() diff --git a/src/fsharp/absil/ilread.fsi b/src/fsharp/absil/ilread.fsi index 505bda1475e..97cb687e1ca 100644 --- a/src/fsharp/absil/ilread.fsi +++ b/src/fsharp/absil/ilread.fsi @@ -26,6 +26,7 @@ /// you need. module FSharp.Compiler.AbstractIL.ILBinaryReader +open System.IO open FSharp.Compiler.AbstractIL.IL /// Used to implement a Binary file over native memory, used by Roslyn integration @@ -71,13 +72,20 @@ type public ILModuleReader = /// Open a binary reader, except first copy the entire contents of the binary into /// memory, close the file and ensure any subsequent reads happen from the in-memory store. /// PDB files may not be read with this option. +/// Binary reader is internally cached. val internal OpenILModuleReader: string -> ILReaderOptions -> ILModuleReader val internal ClearAllILModuleReaderCache : unit -> unit /// Open a binary reader based on the given bytes. +/// This binary reader is not internally cached. val internal OpenILModuleReaderFromBytes: fileName:string -> assemblyContents: byte[] -> options: ILReaderOptions -> ILModuleReader +/// Open a binary reader based on the given stream. +/// This binary reader is not internally cached. +/// The binary reader will own the given stream and the stream will be disposed when there are no references to the binary reader. +val internal OpenILModuleReaderFromStream: fileName:string -> peStream: Stream -> options: ILReaderOptions -> ILModuleReader + type internal Statistics = { mutable rawMemoryFileCount : int mutable memoryMapFileOpenedCount : int diff --git a/src/fsharp/service/FSharpCheckerResults.fs b/src/fsharp/service/FSharpCheckerResults.fs index baf08cb8826..3415e52eab0 100644 --- a/src/fsharp/service/FSharpCheckerResults.fs +++ b/src/fsharp/service/FSharpCheckerResults.fs @@ -59,28 +59,36 @@ type FSharpUnresolvedReferencesSet = FSharpUnresolvedReferencesSet of Unresolved [] type FSharpReferencedProject = | FSharpReference of projectFileName: string * options: FSharpProjectOptions - | ILReference of projectFileName: string * stamp: DateTime * lazyData: Lazy + | PEReference of projectFileName: string * stamp: DateTime * reader: ILModuleReader member this.IsFSharp = match this with | FSharpReference _ -> true | _ -> false - member this.IsIL = + member this.IsPortableExecutable = match this with - | ILReference _ -> true + | PEReference _ -> true | _ -> false member this.ProjectFileName = match this with | FSharpReference(projectFileName=projectFileName) - | ILReference(projectFileName=projectFileName) -> projectFileName + | PEReference(projectFileName=projectFileName) -> projectFileName static member CreateFSharp(projectFileName, options) = FSharpReference(projectFileName, options) - static member CreateIL(projectFileName, stamp, lazyData) = - ILReference(projectFileName, stamp, lazyData) + static member CreatePortableExecutable(projectFileName, stamp, stream) = + let ilReaderOptions: ILReaderOptions = + { + pdbDirPath = None + reduceMemoryUsage = ReduceMemoryFlag.Yes + metadataOnly = MetadataOnlyFlag.Yes + tryGetMetadataSnapshot = fun _ -> None + } + let ilReader = ILBinaryReader.OpenILModuleReaderFromStream projectFileName stream ilReaderOptions + PEReference(projectFileName, stamp, ilReader) // NOTE: may be better just to move to optional arguments here and FSharpProjectOptions = @@ -121,7 +129,7 @@ and FSharpProjectOptions = match r1, r2 with | FSharpReferencedProject.FSharpReference(n1,a), FSharpReferencedProject.FSharpReference(n2,b) -> n1 = n2 && FSharpProjectOptions.AreSameForChecking(a,b) - | FSharpReferencedProject.ILReference(n1, stamp1, _), FSharpReferencedProject.ILReference(n2, stamp2, _) -> + | FSharpReferencedProject.PEReference(n1, stamp1, _), FSharpReferencedProject.PEReference(n2, stamp2, _) -> n1 = n2 && stamp1 = stamp2 | _ -> false) && diff --git a/src/fsharp/service/FSharpCheckerResults.fsi b/src/fsharp/service/FSharpCheckerResults.fsi index 794706b719b..b37c8d74873 100644 --- a/src/fsharp/service/FSharpCheckerResults.fsi +++ b/src/fsharp/service/FSharpCheckerResults.fsi @@ -3,9 +3,11 @@ namespace FSharp.Compiler.CodeAnalysis open System +open System.IO open System.Threading open Internal.Utilities.Library open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler.AbstractIL.ILBinaryReader open FSharp.Compiler.AccessibilityLogic open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CheckDeclarations @@ -85,17 +87,20 @@ type public FSharpProjectOptions = and [] public FSharpReferencedProject = internal | FSharpReference of projectFileName: string * options: FSharpProjectOptions - | ILReference of projectFileName: string * stamp: DateTime * lazyData: Lazy + | PEReference of projectFileName: string * stamp: DateTime * reader: ILModuleReader member IsFSharp : bool - member IsIL : bool + member IsPortableExecutable : bool member ProjectFileName : string + /// Creates a reference for an F# project. The physical data for it is stored/cached inside of the compiler service. static member CreateFSharp : projectFileName: string * options: FSharpProjectOptions -> FSharpReferencedProject - static member CreateIL : projectFileName: string * stamp: DateTime * lazyData: Lazy -> FSharpReferencedProject + /// Creates a reference for any portable executable, including F#. The stream is owned by this reference. + /// The stream will be automatically disposed when there are no references to FSharpReferencedProject and is GC collected. + static member CreatePortableExecutable : projectFileName: string * stamp: DateTime * stream: Stream -> FSharpReferencedProject /// Represents the use of an F# symbol from F# source code [] diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 01849e454dd..256d2b1b573 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -264,7 +264,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC self.TryGetLogicalTimeStampForProject(cache, opts) member x.FileName = nm } - | FSharpReferencedProject.ILReference(nm,stamp,lazyData) -> + | FSharpReferencedProject.PEReference(nm,stamp,stream) -> yield { new IProjectReference with member x.EvaluateRawContents(_) = @@ -276,9 +276,9 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC metadataOnly = MetadataOnlyFlag.Yes tryGetMetadataSnapshot = fun _ -> None } - let ilReader = ILBinaryReader.OpenILModuleReaderFromBytes nm lazyData.Value ilReaderOptions + let ilReader = ILBinaryReader.OpenILModuleReaderFromStream nm stream ilReaderOptions let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs - return RawILAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData |> Some + return RawFSharpAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData |> Some } member x.TryGetLogicalTimeStamp(_) = stamp |> Some member x.FileName = nm } diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 49b39a56c91..4d07c24488c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -183,14 +183,13 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor else let! ilComp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask referencedProjects.Add( - FSharpReferencedProject.CreateIL( + FSharpReferencedProject.CreatePortableExecutable( referencedProject.OutputFilePath, DateTime.UtcNow, - lazy - let ms = new MemoryStream() - let emitOptions = Emit.EmitOptions(metadataOnly = true) - ilComp.Emit(ms, options = emitOptions) |> ignore - ms.ToArray() // REVIEW: Not performant but works until we figure a better way to get the data. + let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. + let emitOptions = Emit.EmitOptions(metadataOnly = true) + ilComp.Emit(ms, options = emitOptions, cancellationToken = ct) |> ignore + ms )) if canBail then From 3bba3a80741987c565b3ae6a580b7f9b4696fd7e Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 13:00:16 -0700 Subject: [PATCH 04/10] Removing a few API endpoints --- src/fsharp/service/FSharpCheckerResults.fs | 12 +----------- src/fsharp/service/FSharpCheckerResults.fsi | 6 +----- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/fsharp/service/FSharpCheckerResults.fs b/src/fsharp/service/FSharpCheckerResults.fs index 3415e52eab0..f5483f25772 100644 --- a/src/fsharp/service/FSharpCheckerResults.fs +++ b/src/fsharp/service/FSharpCheckerResults.fs @@ -61,17 +61,7 @@ type FSharpReferencedProject = | FSharpReference of projectFileName: string * options: FSharpProjectOptions | PEReference of projectFileName: string * stamp: DateTime * reader: ILModuleReader - member this.IsFSharp = - match this with - | FSharpReference _ -> true - | _ -> false - - member this.IsPortableExecutable = - match this with - | PEReference _ -> true - | _ -> false - - member this.ProjectFileName = + member this.FileName = match this with | FSharpReference(projectFileName=projectFileName) | PEReference(projectFileName=projectFileName) -> projectFileName diff --git a/src/fsharp/service/FSharpCheckerResults.fsi b/src/fsharp/service/FSharpCheckerResults.fsi index b37c8d74873..bf1971b11f9 100644 --- a/src/fsharp/service/FSharpCheckerResults.fsi +++ b/src/fsharp/service/FSharpCheckerResults.fsi @@ -89,11 +89,7 @@ and [] public FSharpReferencedProject = | FSharpReference of projectFileName: string * options: FSharpProjectOptions | PEReference of projectFileName: string * stamp: DateTime * reader: ILModuleReader - member IsFSharp : bool - - member IsPortableExecutable : bool - - member ProjectFileName : string + member FileName : string /// Creates a reference for an F# project. The physical data for it is stored/cached inside of the compiler service. static member CreateFSharp : projectFileName: string * options: FSharpProjectOptions -> FSharpReferencedProject From e096f03b5461cc9e4986ab6e62124b2b7ff19986 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 13:06:51 -0700 Subject: [PATCH 05/10] Fixing build --- src/fsharp/service/service.fs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 256d2b1b573..f161c9c2c4e 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -264,19 +264,11 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC self.TryGetLogicalTimeStampForProject(cache, opts) member x.FileName = nm } - | FSharpReferencedProject.PEReference(nm,stamp,stream) -> + | FSharpReferencedProject.PEReference(nm,stamp,ilReader) -> yield { new IProjectReference with member x.EvaluateRawContents(_) = cancellable { - let ilReaderOptions: ILReaderOptions = - { - pdbDirPath = None - reduceMemoryUsage = ReduceMemoryFlag.Yes - metadataOnly = MetadataOnlyFlag.Yes - tryGetMetadataSnapshot = fun _ -> None - } - let ilReader = ILBinaryReader.OpenILModuleReaderFromStream nm stream ilReaderOptions let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs return RawFSharpAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData |> Some } From 1bab6356ed61e16173ad92039e7c40476fdea34b Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 14:57:31 -0700 Subject: [PATCH 06/10] Updating surface area --- .../SurfaceArea.netstandard.fs | 19 +++++++++++++++---- .../FSharpProjectOptionsManager.fs | 3 ++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs b/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs index f932e92fdfe..bb0c7f62a78 100644 --- a/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs +++ b/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs @@ -1947,8 +1947,8 @@ FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FShar FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpDisplayContext] GetDisplayContextForPos(FSharp.Compiler.Text.Position) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpImplementationFileContents] ImplementationFile FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpImplementationFileContents] get_ImplementationFile() -FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.FSharpSymbolUse]] GetMethodsAsSymbols(Int32, Int32, System.String, Microsoft.FSharp.Collections.FSharpList`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText] GenerateSignature() +FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.FSharpSymbolUse]] GetMethodsAsSymbols(Int32, Int32, System.String, Microsoft.FSharp.Collections.FSharpList`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Microsoft.FSharp.Core.FSharpOption`1[System.String] GetF1Keyword(Int32, Int32, System.String, Microsoft.FSharp.Collections.FSharpList`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: System.Collections.Generic.IEnumerable`1[FSharp.Compiler.CodeAnalysis.FSharpSymbolUse] GetAllUsesOfAllSymbolsInFile(Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: System.String ToString() @@ -2101,6 +2101,8 @@ FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Boolean IsIncompleteTypeCheck FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Boolean UseScriptResolutionRules FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Boolean get_IsIncompleteTypeCheckEnvironment() FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Boolean get_UseScriptResolutionRules() +FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: FSharp.Compiler.CodeAnalysis.FSharpReferencedProject[] ReferencedProjects +FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: FSharp.Compiler.CodeAnalysis.FSharpReferencedProject[] get_ReferencedProjects() FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Int32 GetHashCode() FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Int32 GetHashCode(System.Collections.IEqualityComparer) FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]] OriginalLoadReferences @@ -2120,9 +2122,18 @@ FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: System.String[] OtherOptions FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: System.String[] SourceFiles FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: System.String[] get_OtherOptions() FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: System.String[] get_SourceFiles() -FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions][] ReferencedProjects -FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions][] get_ReferencedProjects() -FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Void .ctor(System.String, Microsoft.FSharp.Core.FSharpOption`1[System.String], System.String[], System.String[], System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions][], Boolean, Boolean, System.DateTime, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpUnresolvedReferencesSet], Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]], Microsoft.FSharp.Core.FSharpOption`1[System.Int64]) +FSharp.Compiler.CodeAnalysis.FSharpProjectOptions: Void .ctor(System.String, Microsoft.FSharp.Core.FSharpOption`1[System.String], System.String[], System.String[], FSharp.Compiler.CodeAnalysis.FSharpReferencedProject[], Boolean, Boolean, System.DateTime, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpUnresolvedReferencesSet], Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]], Microsoft.FSharp.Core.FSharpOption`1[System.Int64]) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: Boolean Equals(FSharp.Compiler.CodeAnalysis.FSharpReferencedProject) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: Boolean Equals(System.Object) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: Boolean Equals(System.Object, System.Collections.IEqualityComparer) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: FSharp.Compiler.CodeAnalysis.FSharpReferencedProject CreateFSharp(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: FSharp.Compiler.CodeAnalysis.FSharpReferencedProject CreatePortableExecutable(System.String, System.DateTime, System.IO.Stream) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: Int32 GetHashCode() +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: System.String FileName +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: System.String ToString() +FSharp.Compiler.CodeAnalysis.FSharpReferencedProject: System.String get_FileName() FSharp.Compiler.CodeAnalysis.FSharpSymbolUse FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsFromAttribute FSharp.Compiler.CodeAnalysis.FSharpSymbolUse: Boolean IsFromComputationExpression diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 4d07c24488c..aa39bc7f52e 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -187,8 +187,9 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor referencedProject.OutputFilePath, DateTime.UtcNow, let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. - let emitOptions = Emit.EmitOptions(metadataOnly = true) + let emitOptions = Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false) ilComp.Emit(ms, options = emitOptions, cancellationToken = ct) |> ignore + ms.Position <- 0L ms )) From eea34eb88b14551f67d1a1f6dbf3dfd9d239fa8f Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 15:14:55 -0700 Subject: [PATCH 07/10] Added ConditionalWeakTable for compilation emission --- .../FSharpProjectOptionsManager.fs | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index aa39bc7f52e..70423080f6c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -22,6 +22,7 @@ open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.LanguageServices.Implementation.TaskList open Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices open Microsoft.VisualStudio.FSharp.Interactive.Session +open System.Runtime.CompilerServices [] module private FSharpProjectOptionsHelpers = @@ -103,6 +104,26 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor let cache = ConcurrentDictionary() let singleFileCache = ConcurrentDictionary() + // This is used to not constantly emit the same compilation. + let weakPEReferences = ConditionalWeakTable() + + let createPEReference (referencedProject: Project) (comp: Compilation) ct = + match weakPEReferences.TryGetValue comp with + | true, fsRefProj -> fsRefProj + | _ -> + let fsRefProj = + FSharpReferencedProject.CreatePortableExecutable( + referencedProject.OutputFilePath, + DateTime.UtcNow, + let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. + let emitOptions = Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false, tolerateErrors = true) + comp.Emit(ms, options = emitOptions, cancellationToken = ct) |> ignore + ms.Position <- 0L + ms + ) + weakPEReferences.Add(comp, fsRefProj) + fsRefProj + let rec tryComputeOptionsByFile (document: Document) (ct: CancellationToken) userOpName = async { let! fileStamp = document.GetTextVersionAsync(ct) |> Async.AwaitTask @@ -181,17 +202,8 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor | None -> canBail <- true | Some(_, projectOptions) -> referencedProjects.Add(FSharpReferencedProject.CreateFSharp(referencedProject.OutputFilePath, projectOptions)) else - let! ilComp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask - referencedProjects.Add( - FSharpReferencedProject.CreatePortableExecutable( - referencedProject.OutputFilePath, - DateTime.UtcNow, - let ms = new MemoryStream() // do not dispose the stream as it will be owned on the reference. - let emitOptions = Emit.EmitOptions(metadataOnly = true, includePrivateMembers = false) - ilComp.Emit(ms, options = emitOptions, cancellationToken = ct) |> ignore - ms.Position <- 0L - ms - )) + let! comp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask + referencedProjects.Add(createPEReference referencedProject comp ct) if canBail then return None From 3ae5fdf6d281bf6d20f3c7b6e16ed7264d0798c5 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Apr 2021 15:58:52 -0700 Subject: [PATCH 08/10] Just in case, put a try/with around creating a pe reference --- .../LanguageService/FSharpProjectOptionsManager.fs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 70423080f6c..28b1033d6bc 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -203,7 +203,11 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor | Some(_, projectOptions) -> referencedProjects.Add(FSharpReferencedProject.CreateFSharp(referencedProject.OutputFilePath, projectOptions)) else let! comp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask - referencedProjects.Add(createPEReference referencedProject comp ct) + try + let peRef = createPEReference referencedProject comp ct + referencedProjects.Add(peRef) + with + | _ -> () if canBail then return None From 52d0550a179f669d6743caf3dea162c8cafd4e70 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 7 Apr 2021 13:29:37 -0700 Subject: [PATCH 09/10] Remove try/with --- .../LanguageService/FSharpProjectOptionsManager.fs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 28b1033d6bc..34a587bf587 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -203,11 +203,8 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor | Some(_, projectOptions) -> referencedProjects.Add(FSharpReferencedProject.CreateFSharp(referencedProject.OutputFilePath, projectOptions)) else let! comp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask - try - let peRef = createPEReference referencedProject comp ct - referencedProjects.Add(peRef) - with - | _ -> () + let peRef = createPEReference referencedProject comp ct + referencedProjects.Add(peRef) if canBail then return None From d07b4eab0e49819a81aaf2c453f8418d8c692afe Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 8 Apr 2021 13:51:33 -0700 Subject: [PATCH 10/10] Adding C# in-memory reference for compiler service tests --- tests/FSharp.Test.Utilities/CompilerAssert.fs | 4 + tests/FSharp.Test.Utilities/Utilities.fs | 4 +- .../Compiler/Service/MultiProjectTests.fs | 82 +++++++++++++++++++ tests/fsharp/FSharpSuite.Tests.fsproj | 1 + 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/fsharp/Compiler/Service/MultiProjectTests.fs diff --git a/tests/FSharp.Test.Utilities/CompilerAssert.fs b/tests/FSharp.Test.Utilities/CompilerAssert.fs index da4f477b7d2..42b123647fb 100644 --- a/tests/FSharp.Test.Utilities/CompilerAssert.fs +++ b/tests/FSharp.Test.Utilities/CompilerAssert.fs @@ -460,6 +460,10 @@ let main argv = 0""" let exitCode, output, errors = Commands.executeProcess (Some filename) arguments (Path.GetDirectoryName(outputFilePath)) timeout (exitCode, output |> String.concat "\n", errors |> String.concat "\n") + static member Checker = checker + + static member DefaultProjectOptions = defaultProjectOptions + static member CompileWithErrors(cmpl: Compilation, expectedErrors, ?ignoreWarnings) = let ignoreWarnings = defaultArg ignoreWarnings false lock gate (fun () -> diff --git a/tests/FSharp.Test.Utilities/Utilities.fs b/tests/FSharp.Test.Utilities/Utilities.fs index fe192cce0e8..762849037cf 100644 --- a/tests/FSharp.Test.Utilities/Utilities.fs +++ b/tests/FSharp.Test.Utilities/Utilities.fs @@ -75,7 +75,7 @@ module Utilities = let systemConsoleRef = lazy AssemblyMetadata.CreateFromImage(NetCoreApp31Refs.System_Console ()).GetReference(display = "System.Console.dll (netcoreapp 3.1 ref)") [] - module internal TargetFrameworkUtil = + module TargetFrameworkUtil = open TestReferences @@ -84,7 +84,7 @@ module Utilities = let private netCoreApp31References = lazy ImmutableArray.Create(NetCoreApp31.netStandard.Value, NetCoreApp31.mscorlibRef.Value, NetCoreApp31.systemRuntimeRef.Value, NetCoreApp31.systemCoreRef.Value, NetCoreApp31.systemDynamicRuntimeRef.Value, NetCoreApp31.systemConsoleRef.Value) - let internal getReferences tf = + let getReferences tf = match tf with | TargetFramework.NetStandard20 -> netStandard20References.Value | TargetFramework.NetCoreApp31 -> netCoreApp31References.Value diff --git a/tests/fsharp/Compiler/Service/MultiProjectTests.fs b/tests/fsharp/Compiler/Service/MultiProjectTests.fs new file mode 100644 index 00000000000..aedf57592c4 --- /dev/null +++ b/tests/fsharp/Compiler/Service/MultiProjectTests.fs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Compiler.UnitTests + +open System +open System.IO +open FSharp.Compiler.Diagnostics +open NUnit.Framework +open FSharp.Test.Utilities +open FSharp.Test.Utilities.Utilities +open FSharp.Test.Utilities.Compiler +open FSharp.Tests +open FSharp.Compiler.CodeAnalysis +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CSharp +open FSharp.Compiler.Text + +[] +module MultiProjectTests = + + let AssertInMemoryCSharpReferenceIsValid () = + let csSrc = + """ +namespace CSharpTest +{ + public class CSharpClass + { + } +} + """ + + let csOptions = CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + let csSyntax = CSharpSyntaxTree.ParseText(csSrc) + let csReferences = TargetFrameworkUtil.getReferences TargetFramework.NetStandard20 + let cs = CSharpCompilation.Create("csharp_test.dll", references = csReferences.As(), syntaxTrees = [csSyntax], options = csOptions) + let ms = new MemoryStream() + let result = cs.Emit(ms) + Assert.shouldBeEmpty(result.Diagnostics) + ms.Position <- 0L + let csRefProj = FSharpReferencedProject.CreatePortableExecutable("""Z:\csharp_test.dll""", DateTime.UtcNow, ms) + + let fsOptions = CompilerAssert.DefaultProjectOptions + let fsOptions = + { fsOptions with + ProjectId = Some(Guid.NewGuid().ToString()) + OtherOptions = Array.append fsOptions.OtherOptions [|"""-r:Z:\csharp_test.dll"""|] + ReferencedProjects = [|csRefProj|] } + + let fsText = + """ +module FSharpTest + +open CSharpTest + +let test() = + CSharpClass() + """ + |> SourceText.ofString + let _, checkAnswer = + CompilerAssert.Checker.ParseAndCheckFileInProject("test.fs", 0, fsText, fsOptions) + |> Async.RunSynchronously + + + match checkAnswer with + | FSharpCheckFileAnswer.Aborted -> failwith "check file aborted" + | FSharpCheckFileAnswer.Succeeded(checkResults) -> + Assert.shouldBeEmpty(checkResults.Diagnostics) + WeakReference(ms) + + [] + let ``Using a CSharp reference project in-memory``() = + AssertInMemoryCSharpReferenceIsValid() |> ignore + + [] + let ``Using a CSharp reference project in-memory and it gets GCed``() = + let weakRef = AssertInMemoryCSharpReferenceIsValid() + CompilerAssert.Checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + GC.Collect(2, GCCollectionMode.Forced, true) + Assert.shouldBeFalse(weakRef.IsAlive) + + + diff --git a/tests/fsharp/FSharpSuite.Tests.fsproj b/tests/fsharp/FSharpSuite.Tests.fsproj index 952372043cf..6ba0c4a6a5d 100644 --- a/tests/fsharp/FSharpSuite.Tests.fsproj +++ b/tests/fsharp/FSharpSuite.Tests.fsproj @@ -25,6 +25,7 @@ +