diff --git a/src/fsharp/BuildGraph.fs b/src/fsharp/BuildGraph.fs new file mode 100644 index 0000000000..d8fe2d1483 --- /dev/null +++ b/src/fsharp/BuildGraph.fs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +module FSharp.Compiler.BuildGraph + +open System +open System.Threading +open System.Threading.Tasks +open System.Diagnostics +open System.Globalization +open FSharp.Compiler.ErrorLogger +open Internal.Utilities.Library + +/// This represents the thread-local state established as each task function runs as part of the build. +/// +/// Use to reset error and warning handlers. +type CompilationGlobalsScope(errorLogger: ErrorLogger, phase: BuildPhase) = + let unwindEL = PushErrorLoggerPhaseUntilUnwind(fun _ -> errorLogger) + let unwindBP = PushThreadBuildPhaseUntilUnwind phase + + member _.ErrorLogger = errorLogger + member _.Phase = phase + + // Return the disposable object that cleans up + interface IDisposable with + member d.Dispose() = + unwindBP.Dispose() + unwindEL.Dispose() + +[] +type NodeCode<'T> = Node of Async<'T> + +let wrapThreadStaticInfo computation = + async { + let errorLogger = CompileThreadStatic.ErrorLogger + let phase = CompileThreadStatic.BuildPhase + try + return! computation + finally + CompileThreadStatic.ErrorLogger <- errorLogger + CompileThreadStatic.BuildPhase <- phase + } + +type Async<'T> with + + static member AwaitNodeCode(node: NodeCode<'T>) = + match node with + | Node(computation) -> wrapThreadStaticInfo computation + +[] +type NodeCodeBuilder() = + + static let zero = Node(async.Zero()) + + [] + member _.Zero () : NodeCode = zero + + [] + member _.Delay (f: unit -> NodeCode<'T>) = + Node(async.Delay(fun () -> match f() with Node(p) -> p)) + + [] + member _.Return value = Node(async.Return(value)) + + [] + member _.ReturnFrom (computation: NodeCode<_>) = computation + + [] + member _.Bind (Node(p): NodeCode<'a>, binder: 'a -> NodeCode<'b>) : NodeCode<'b> = + Node(async.Bind(p, fun x -> match binder x with Node p -> p)) + + [] + member _.TryWith(Node(p): NodeCode<'T>, binder: exn -> NodeCode<'T>) : NodeCode<'T> = + Node(async.TryWith(p, fun ex -> match binder ex with Node p -> p)) + + [] + member _.TryFinally(Node(p): NodeCode<'T>, binder: unit -> unit) : NodeCode<'T> = + Node(async.TryFinally(p, binder)) + + [] + member _.For(xs: 'T seq, binder: 'T -> NodeCode) : NodeCode = + Node(async.For(xs, fun x -> match binder x with Node p -> p)) + + [] + member _.Combine(Node(p1): NodeCode, Node(p2): NodeCode<'T>) : NodeCode<'T> = + Node(async.Combine(p1, p2)) + + [] + member _.Using(value: CompilationGlobalsScope, binder: CompilationGlobalsScope -> NodeCode<'U>) = + Node( + async { + CompileThreadStatic.ErrorLogger <- value.ErrorLogger + CompileThreadStatic.BuildPhase <- value.Phase + try + return! binder value |> Async.AwaitNodeCode + finally + (value :> IDisposable).Dispose() + } + ) + +let node = NodeCodeBuilder() + +[] +type NodeCode private () = + + static let cancellationToken = + Node(wrapThreadStaticInfo Async.CancellationToken) + + static member RunImmediate (computation: NodeCode<'T>, ct: CancellationToken) = + let errorLogger = CompileThreadStatic.ErrorLogger + let phase = CompileThreadStatic.BuildPhase + try + try + let work = + async { + CompileThreadStatic.ErrorLogger <- errorLogger + CompileThreadStatic.BuildPhase <- phase + return! computation |> Async.AwaitNodeCode + } + Async.StartImmediateAsTask(work, cancellationToken=ct).Result + finally + CompileThreadStatic.ErrorLogger <- errorLogger + CompileThreadStatic.BuildPhase <- phase + with + | :? AggregateException as ex when ex.InnerExceptions.Count = 1 -> + raise(ex.InnerExceptions.[0]) + + static member RunImmediateWithoutCancellation (computation: NodeCode<'T>) = + NodeCode.RunImmediate(computation, CancellationToken.None) + + static member StartAsTask_ForTesting (computation: NodeCode<'T>, ?ct: CancellationToken) = + let errorLogger = CompileThreadStatic.ErrorLogger + let phase = CompileThreadStatic.BuildPhase + try + let work = + async { + CompileThreadStatic.ErrorLogger <- errorLogger + CompileThreadStatic.BuildPhase <- phase + return! computation |> Async.AwaitNodeCode + } + Async.StartAsTask(work, cancellationToken=defaultArg ct CancellationToken.None) + finally + CompileThreadStatic.ErrorLogger <- errorLogger + CompileThreadStatic.BuildPhase <- phase + + static member CancellationToken = cancellationToken + + static member FromCancellable(computation: Cancellable<'T>) = + Node(wrapThreadStaticInfo (Cancellable.toAsync computation)) + + static member AwaitAsync(computation: Async<'T>) = + Node(wrapThreadStaticInfo computation) + + static member AwaitTask(task: Task<'T>) = + Node(wrapThreadStaticInfo(Async.AwaitTask task)) + + static member AwaitTask(task: Task) = + Node(wrapThreadStaticInfo(Async.AwaitTask task)) + + static member AwaitWaitHandle_ForTesting(waitHandle: WaitHandle) = + Node(wrapThreadStaticInfo (Async.AwaitWaitHandle(waitHandle))) + + static member Sleep(ms: int) = + Node(wrapThreadStaticInfo (Async.Sleep(ms))) + + static member Sequential(computations: NodeCode<'T> seq) = + node { + let results = ResizeArray() + for computation in computations do + let! res = computation + results.Add(res) + return results.ToArray() + } + +type private AgentMessage<'T> = + | GetValue of AsyncReplyChannel> * callerCancellationToken: CancellationToken + +type private Agent<'T> = (MailboxProcessor> * CancellationTokenSource) + +[] +type private GraphNodeAction<'T> = + | GetValueByAgent + | GetValue + | CachedValue of 'T + +[] +module GraphNode = + + // We need to store the culture for the VS thread that is executing now, + // so that when the agent in the async lazy object picks up thread from the thread pool we can set the culture + let mutable culture = CultureInfo(CultureInfo.CurrentUICulture.Name) + + let SetPreferredUILang (preferredUiLang: string option) = + match preferredUiLang with + | Some s -> + culture <- CultureInfo s +#if FX_RESHAPED_GLOBALIZATION + CultureInfo.CurrentUICulture <- culture +#else + Thread.CurrentThread.CurrentUICulture <- culture +#endif + | None -> () + +[] +type GraphNode<'T> (retryCompute: bool, computation: NodeCode<'T>) = + + let gate = obj () + let mutable computation = computation + let mutable requestCount = 0 + let mutable cachedResult: Task<'T> = Unchecked.defaultof<_> + let mutable cachedResultNode: NodeCode<'T> = Unchecked.defaultof<_> + + let isCachedResultNodeNotNull() = + not (obj.ReferenceEquals(cachedResultNode, null)) + + let isCachedResultNotNull() = + cachedResult <> null + + // retryCompute indicates that we abandon computations when the originator is + // cancelled. + // + // If retryCompute is 'true', the computation is run directly in the originating requestor's + // thread. If cancelled, other awaiting computations must restart the computation from scratch. + // + // If retryCompute is 'false', a MailboxProcessor is used to allow the cancelled originator + // to detach from the computation, while other awaiting computations continue to wait on the result. + // + // Currently, 'retryCompute' = true for all graph nodes. However, the code for we include the + // code to allow 'retryCompute' = false in case it's needed in the future, and ensure it is under independent + // unit test. + let loop (agent: MailboxProcessor>) = + async { + assert (not retryCompute) + try + while true do + match! agent.Receive() with + | GetValue (replyChannel, callerCancellationToken) -> + + Thread.CurrentThread.CurrentUICulture <- GraphNode.culture + try + use _reg = + // When a cancellation has occured, notify the reply channel to let the requester stop waiting for a response. + callerCancellationToken.Register (fun () -> + let ex = OperationCanceledException() :> exn + replyChannel.Reply (Result.Error ex) + ) + + callerCancellationToken.ThrowIfCancellationRequested () + + if isCachedResultNotNull() then + replyChannel.Reply(Ok cachedResult.Result) + else + // This computation can only be canceled if the requestCount reaches zero. + let! result = computation |> Async.AwaitNodeCode + cachedResult <- Task.FromResult(result) + cachedResultNode <- node { return result } + computation <- Unchecked.defaultof<_> + if not callerCancellationToken.IsCancellationRequested then + replyChannel.Reply(Ok result) + with + | ex -> + if not callerCancellationToken.IsCancellationRequested then + replyChannel.Reply(Result.Error ex) + with + | _ -> + () + } + + let mutable agent: Agent<'T> = Unchecked.defaultof<_> + + let semaphore: SemaphoreSlim = + if retryCompute then + new SemaphoreSlim(1, 1) + else + Unchecked.defaultof<_> + + member _.GetOrComputeValue() = + // fast path + if isCachedResultNodeNotNull() then + cachedResultNode + else + node { + if isCachedResultNodeNotNull() then + return! cachedResult |> NodeCode.AwaitTask + else + let action = + lock gate <| fun () -> + // We try to get the cached result after the lock so we don't spin up a new mailbox processor. + if isCachedResultNodeNotNull() then + GraphNodeAction<'T>.CachedValue cachedResult.Result + else + requestCount <- requestCount + 1 + if retryCompute then + GraphNodeAction<'T>.GetValue + else + match box agent with + | null -> + try + let cts = new CancellationTokenSource() + let mbp = new MailboxProcessor<_>(loop, cancellationToken = cts.Token) + let newAgent = (mbp, cts) + agent <- newAgent + mbp.Start() + GraphNodeAction<'T>.GetValueByAgent + with + | ex -> + agent <- Unchecked.defaultof<_> + raise ex + | _ -> + GraphNodeAction<'T>.GetValueByAgent + + match action with + | GraphNodeAction.CachedValue result -> return result + | GraphNodeAction.GetValue -> + try + let! ct = NodeCode.CancellationToken + + // We must set 'taken' before any implicit cancellation checks + // occur, making sure we are under the protection of the 'try'. + // For example, NodeCode's 'try/finally' (TryFinally) uses async.TryFinally which does + // implicit cancellation checks even before the try is entered, as do the + // de-sugaring of 'do!' and other CodeCode constructs. + let mutable taken = false + try + do! + semaphore.WaitAsync(ct) + .ContinueWith( + (fun _ -> taken <- true), + (TaskContinuationOptions.NotOnCanceled ||| TaskContinuationOptions.NotOnFaulted ||| TaskContinuationOptions.ExecuteSynchronously) + ) + |> NodeCode.AwaitTask + + if isCachedResultNotNull() then + return cachedResult.Result + else + let tcs = TaskCompletionSource<'T>() + let (Node(p)) = computation + Async.StartWithContinuations( + async { + Thread.CurrentThread.CurrentUICulture <- GraphNode.culture + return! p + }, + (fun res -> + cachedResult <- Task.FromResult(res) + cachedResultNode <- node { return res } + computation <- Unchecked.defaultof<_> + tcs.SetResult(res) + ), + (fun ex -> + tcs.SetException(ex) + ), + (fun _ -> + tcs.SetCanceled() + ), + ct + ) + return! tcs.Task |> NodeCode.AwaitTask + finally + if taken then + semaphore.Release() |> ignore + finally + lock gate <| fun () -> + requestCount <- requestCount - 1 + + | GraphNodeAction.GetValueByAgent -> + assert (not retryCompute) + let mbp, cts = agent + try + let! ct = NodeCode.CancellationToken + let! res = mbp.PostAndAsyncReply(fun replyChannel -> GetValue(replyChannel, ct)) |> NodeCode.AwaitAsync + match res with + | Ok result -> return result + | Result.Error ex -> return raise ex + finally + lock gate <| fun () -> + requestCount <- requestCount - 1 + if requestCount = 0 then + cts.Cancel() // cancel computation when all requests are cancelled + try (mbp :> IDisposable).Dispose () with | _ -> () + cts.Dispose() + agent <- Unchecked.defaultof<_> + } + + member _.TryPeekValue() = + match cachedResult with + | null -> ValueNone + | _ -> ValueSome cachedResult.Result + + member _.HasValue = cachedResult <> null + + member _.IsComputing = requestCount > 0 + + new(computation) = + GraphNode(retryCompute=true, computation=computation) \ No newline at end of file diff --git a/src/fsharp/BuildGraph.fsi b/src/fsharp/BuildGraph.fsi new file mode 100644 index 0000000000..cf1d750c3e --- /dev/null +++ b/src/fsharp/BuildGraph.fsi @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +module internal FSharp.Compiler.BuildGraph + +open System +open System.Threading +open System.Threading.Tasks +open FSharp.Compiler.ErrorLogger +open Internal.Utilities.Library + +/// This represents the global state established as each task function runs as part of the build. +/// +/// Use to reset error and warning handlers. +type CompilationGlobalsScope = + new : ErrorLogger * BuildPhase -> CompilationGlobalsScope + interface IDisposable + +/// Represents code that can be run as part of the build graph. +/// +/// This is essentially cancellable async code where the only asynchronous waits are on nodes. +/// When a node is evaluated the evaluation is run synchronously on the thread of the +/// first requestor. +[] +type NodeCode<'T> + +type Async<'T> with + + /// Asynchronously await code in the build graph + static member AwaitNodeCode: node: NodeCode<'T> -> Async<'T> + +/// A standard builder for node code. +[] +type NodeCodeBuilder = + + member Bind : NodeCode<'T> * ('T -> NodeCode<'U>) -> NodeCode<'U> + + member Zero : unit -> NodeCode + + member Delay : (unit -> NodeCode<'T>) -> NodeCode<'T> + + member Return : 'T -> NodeCode<'T> + + member ReturnFrom : NodeCode<'T> -> NodeCode<'T> + + member TryWith : NodeCode<'T> * (exn -> NodeCode<'T>) -> NodeCode<'T> + + member TryFinally : NodeCode<'T> * (unit -> unit) -> NodeCode<'T> + + member For : xs: 'T seq * binder: ('T -> NodeCode) -> NodeCode + + member Combine : x1: NodeCode * x2: NodeCode<'T> -> NodeCode<'T> + + /// A limited form 'use' for establishing the compilation globals. (Note + /// that a proper generic 'use' could be implemented but has not currently been necessary) + member Using : CompilationGlobalsScope * (CompilationGlobalsScope -> NodeCode<'T>) -> NodeCode<'T> + +/// Specifies code that can be run as part of the build graph. +val node : NodeCodeBuilder + +/// Contains helpers to specify code that can be run as part of the build graph. +[] +type NodeCode = + + /// Only used for testing, do not use + static member RunImmediate: computation: NodeCode<'T> * ct: CancellationToken -> 'T + + /// Used in places where we don't care about cancellation, e.g. the command line compiler + /// and F# Interactive + static member RunImmediateWithoutCancellation: computation: NodeCode<'T> -> 'T + + static member CancellationToken: NodeCode + + static member Sequential: computations: NodeCode<'T> seq -> NodeCode<'T []> + + /// Execute the cancellable computation synchronously using the ambient cancellation token of + /// the NodeCode. + static member FromCancellable: computation: Cancellable<'T> -> NodeCode<'T> + + /// Only used for testing, do not use + static member StartAsTask_ForTesting: computation: NodeCode<'T> * ?ct: CancellationToken -> Task<'T> + + /// Only used for testing, do not use + static member AwaitWaitHandle_ForTesting: waitHandle: WaitHandle -> NodeCode + +/// Contains helpers related to the build graph +[] +module internal GraphNode = + + /// Allows to specify the language for error messages + val SetPreferredUILang: preferredUiLang: string option -> unit + +/// Evaluate the computation, allowing asynchronous waits on existing ongoing evaluations of the +/// same node, and strongly cache the result. +/// +/// Once the result has been cached, the computation function will also be removed, or 'null'ed out, +/// as to prevent any references captured by the computation from being strongly held. +[] +type internal GraphNode<'T> = + + /// - retryCompute - When set to 'true', subsequent requesters will retry the computation if the first-in request cancels. Retrying computations will have better callstacks. + /// - computation - The computation code to run. + new: retryCompute: bool * computation: NodeCode<'T> -> GraphNode<'T> + + /// By default, 'retryCompute' is 'true'. + new : computation: NodeCode<'T> -> GraphNode<'T> + + /// Return NodeCode which, when executed, will get the value of the computation if already computed, or + /// await an existing in-progress computation for the node if one exists, or else will synchronously + /// start the computation on the current thread. + member GetOrComputeValue: unit -> NodeCode<'T> + + /// Return 'Some' if the computation has already been computed, else None if + /// the computation is in-progress or has not yet been started. + member TryPeekValue: unit -> 'T voption + + /// Return 'true' if the computation has already been computed. + member HasValue: bool + + /// Return 'true' if the computation is in-progress. + member IsComputing: bool \ No newline at end of file diff --git a/src/fsharp/CheckDeclarations.fs b/src/fsharp/CheckDeclarations.fs index 76e40e8b09..526d5fa240 100644 --- a/src/fsharp/CheckDeclarations.fs +++ b/src/fsharp/CheckDeclarations.fs @@ -5003,8 +5003,8 @@ module TcDeclarations = // Bind module types //------------------------------------------------------------------------- -let rec TcSignatureElementNonMutRec cenv parent typeNames endm (env: TcEnv) synSigDecl: Eventually = - eventually { +let rec TcSignatureElementNonMutRec cenv parent typeNames endm (env: TcEnv) synSigDecl: Cancellable = + cancellable { try match synSigDecl with | SynModuleSigDecl.Exception (edef, m) -> @@ -5155,7 +5155,7 @@ let rec TcSignatureElementNonMutRec cenv parent typeNames endm (env: TcEnv) synS and TcSignatureElements cenv parent endm env xml mutRecNSInfo defs = - eventually { + cancellable { // Ensure the .Deref call in UpdateAccModuleOrNamespaceType succeeds if cenv.compilingCanonicalFslibModuleType then let doc = xml.ToXmlDoc(true, Some []) @@ -5170,10 +5170,10 @@ and TcSignatureElements cenv parent endm env xml mutRecNSInfo defs = } and TcSignatureElementsNonMutRec cenv parent typeNames endm env defs = - Eventually.fold (TcSignatureElementNonMutRec cenv parent typeNames endm) env defs + Cancellable.fold (TcSignatureElementNonMutRec cenv parent typeNames endm) env defs and TcSignatureElementsMutRec cenv parent typeNames m mutRecNSInfo envInitial (defs: SynModuleSigDecl list) = - eventually { + cancellable { let m = match defs with [] -> m | _ -> defs |> List.map (fun d -> d.Range) |> List.reduce unionRanges let scopem = (defs, m) ||> List.foldBack (fun h m -> unionRanges h.Range m) @@ -5227,7 +5227,7 @@ and TcSignatureElementsMutRec cenv parent typeNames m mutRecNSInfo envInitial (d and TcModuleOrNamespaceSignatureElementsNonMutRec cenv parent env (id, modKind, defs, m: range, xml) = - eventually { + cancellable { let endm = m.EndRange // use end of range for errors // Create the module type that will hold the results of type checking.... @@ -5287,7 +5287,7 @@ let CheckLetOrDoInNamespace binds m = /// The non-mutually recursive case for a declaration let rec TcModuleOrNamespaceElementNonMutRec (cenv: cenv) parent typeNames scopem env synDecl = - eventually { + cancellable { cenv.synArgNameGenerator.Reset() let tpenv = emptyUnscopedTyparEnv @@ -5468,7 +5468,7 @@ let rec TcModuleOrNamespaceElementNonMutRec (cenv: cenv) parent typeNames scopem /// The non-mutually recursive case for a sequence of declarations and TcModuleOrNamespaceElementsNonMutRec cenv parent typeNames endm (defsSoFar, env, envAtEnd) (moreDefs: SynModuleDecl list) = - eventually { + cancellable { match moreDefs with | (firstDef :: otherDefs) -> // Lookahead one to find out the scope of the next declaration. @@ -5488,7 +5488,7 @@ and TcModuleOrNamespaceElementsNonMutRec cenv parent typeNames endm (defsSoFar, /// The mutually recursive case for a sequence of declarations (and nested modules) and TcModuleOrNamespaceElementsMutRec (cenv: cenv) parent typeNames m envInitial mutRecNSInfo (defs: SynModuleDecl list) = - eventually { + cancellable { let m = match defs with [] -> m | _ -> defs |> List.map (fun d -> d.Range) |> List.reduce unionRanges let scopem = (defs, m) ||> List.foldBack (fun h m -> unionRanges h.Range m) @@ -5578,7 +5578,7 @@ and TcMutRecDefsFinish cenv defs m = TMDefRec(true, tycons, binds, m) and TcModuleOrNamespaceElements cenv parent endm env xml mutRecNSInfo defs = - eventually { + cancellable { // Ensure the deref_nlpath call in UpdateAccModuleOrNamespaceType succeeds if cenv.compilingCanonicalFslibModuleType then let doc = xml.ToXmlDoc(true, Some []) @@ -5798,7 +5798,7 @@ let TypeCheckOneImplFile let infoReader = InfoReader(g, amap) - eventually { + cancellable { let cenv = cenv.Create (g, isScript, niceNameGen, amap, topCcu, false, Option.isSome rootSigOpt, conditionalDefines, tcSink, (LightweightTcValForUsingInBuildMethodCall g), isInternalTestSpanStackReferring, @@ -5904,7 +5904,7 @@ let TypeCheckOneImplFile /// Check an entire signature file let TypeCheckOneSigFile (g, niceNameGen, amap, topCcu, checkForErrors, conditionalDefines, tcSink, isInternalTestSpanStackReferring) tcEnv (ParsedSigFileInput (_, qualNameOfFile, _, _, sigFileFrags)) = - eventually { + cancellable { let cenv = cenv.Create (g, false, niceNameGen, amap, topCcu, true, false, conditionalDefines, tcSink, diff --git a/src/fsharp/CheckDeclarations.fsi b/src/fsharp/CheckDeclarations.fsi index b402618695..ccfc873fd7 100644 --- a/src/fsharp/CheckDeclarations.fsi +++ b/src/fsharp/CheckDeclarations.fsi @@ -39,13 +39,13 @@ val TypeCheckOneImplFile : -> TcEnv -> ModuleOrNamespaceType option -> ParsedImplFileInput - -> Eventually + -> Cancellable val TypeCheckOneSigFile : TcGlobals * NiceNameGenerator * ImportMap * CcuThunk * (unit -> bool) * ConditionalDefines option * NameResolution.TcResultsSink * bool -> TcEnv -> ParsedSigFileInput - -> Eventually + -> Cancellable exception ParameterlessStructCtor of range diff --git a/src/fsharp/CompilerConfig.fs b/src/fsharp/CompilerConfig.fs index 4b4951e301..c7dd623fad 100644 --- a/src/fsharp/CompilerConfig.fs +++ b/src/fsharp/CompilerConfig.fs @@ -25,6 +25,7 @@ open FSharp.Compiler.Text open FSharp.Compiler.Text.Range open FSharp.Compiler.Xml open FSharp.Compiler.TypedTree +open FSharp.Compiler.BuildGraph #if !NO_EXTENSIONTYPING open FSharp.Compiler.ExtensionTyping @@ -205,7 +206,7 @@ and IProjectReference = abstract FileName: string /// Evaluate raw contents of the assembly file generated by the project - abstract EvaluateRawContents: CompilationThreadToken -> Cancellable + abstract EvaluateRawContents: unit -> NodeCode /// Get the logical timestamp that would be the timestamp of the assembly file generated by the project /// diff --git a/src/fsharp/CompilerConfig.fsi b/src/fsharp/CompilerConfig.fsi index 1bb1ffb47b..4c8831e6ba 100644 --- a/src/fsharp/CompilerConfig.fsi +++ b/src/fsharp/CompilerConfig.fsi @@ -19,6 +19,7 @@ open FSharp.Compiler.ErrorLogger open FSharp.Compiler.Features open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Text +open FSharp.Compiler.BuildGraph exception FileNameNotResolved of (*filename*) string * (*description of searched locations*) string * range exception LoadedSourceNotFoundIgnoring of (*filename*) string * range @@ -68,7 +69,7 @@ and IProjectReference = abstract FileName: string /// Evaluate raw contents of the assembly file generated by the project - abstract EvaluateRawContents: CompilationThreadToken -> Cancellable + abstract EvaluateRawContents: unit -> NodeCode /// Get the logical timestamp that would be the timestamp of the assembly file generated by the project. /// diff --git a/src/fsharp/CompilerImports.fs b/src/fsharp/CompilerImports.fs index f61f467c99..c730190ad0 100644 --- a/src/fsharp/CompilerImports.fs +++ b/src/fsharp/CompilerImports.fs @@ -39,6 +39,7 @@ open FSharp.Compiler.TypedTreePickle open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeBasics open FSharp.Compiler.TypedTreeOps +open FSharp.Compiler.BuildGraph #if !NO_EXTENSIONTYPING open FSharp.Compiler.ExtensionTyping @@ -232,42 +233,25 @@ type AssemblyResolution = /// Compute the ILAssemblyRef for a resolved assembly. This is done by reading the binary if necessary. The result /// is cached. /// - /// For project references in the language service, this would result in a build of the project. - /// This is because ``EvaluateRawContents ctok`` is used. However this path is only currently used - /// in fsi.fs, which does not use project references. - // - member this.GetILAssemblyRef(ctok, reduceMemoryUsage, tryGetMetadataSnapshot) = - cancellable { + /// Only used in F# Interactive + member this.GetILAssemblyRef(reduceMemoryUsage, tryGetMetadataSnapshot) = match this.ilAssemblyRef with - | Some assemblyRef -> return assemblyRef + | Some assemblyRef -> assemblyRef | None -> - let! assemblyRefOpt = - cancellable { - match this.ProjectReference with - | Some r -> - let! contents = r.EvaluateRawContents ctok - match contents with - | None -> return None - | Some contents -> - match contents.ILScopeRef with - | ILScopeRef.Assembly aref -> return Some aref - | _ -> return None - | None -> return None - } + match this.ProjectReference with + | Some _ -> failwith "IProjectReference is not allowed to be used in GetILAssemblyRef" + | None -> () + let assemblyRef = - match assemblyRefOpt with - | Some aref -> aref - | None -> - let readerSettings: ILReaderOptions = - { pdbDirPath=None - reduceMemoryUsage = reduceMemoryUsage - metadataOnly = MetadataOnlyFlag.Yes - tryGetMetadataSnapshot = tryGetMetadataSnapshot } - use reader = OpenILModuleReader this.resolvedPath readerSettings - mkRefToILAssembly reader.ILModuleDef.ManifestOfAssembly + let readerSettings: ILReaderOptions = + { pdbDirPath=None + reduceMemoryUsage = reduceMemoryUsage + metadataOnly = MetadataOnlyFlag.Yes + tryGetMetadataSnapshot = tryGetMetadataSnapshot } + use reader = OpenILModuleReader this.resolvedPath readerSettings + mkRefToILAssembly reader.ILModuleDef.ManifestOfAssembly this.ilAssemblyRef <- Some assemblyRef - return assemblyRef - } + assemblyRef type ImportedBinary = { FileName: string @@ -299,6 +283,13 @@ type CcuLoadFailureAction = | RaiseError | ReturnNone +type TcImportsLockToken() = + interface LockToken + +type TcImportsLock = Lock + +let RequireTcImportsLock (_tcitok: TcImportsLockToken, _thingProtected: 'T) = () + type TcConfig with member tcConfig.TryResolveLibWithDirectories (r: AssemblyReference) = @@ -415,7 +406,7 @@ type TcConfig with try tcConfig.legacyReferenceResolver.Impl.Resolve - (tcConfig.resolutionEnvironment, + (tcConfig.resolutionEnvironment, references, tcConfig.targetFrameworkVersion, tcConfig.GetTargetFrameworkDirectories(), @@ -518,7 +509,6 @@ type TcConfig with else resultingResolutions, unresolvedReferences |> List.map (fun (name, _, r) -> (name, r)) |> List.map UnresolvedAssemblyReference - [] type TcAssemblyResolutions(tcConfig: TcConfig, results: AssemblyResolution list, unresolved: UnresolvedAssemblyReference list) = @@ -538,23 +528,23 @@ type TcAssemblyResolutions(tcConfig: TcConfig, results: AssemblyResolution list, member _.TryFindByOriginalReference(assemblyReference: AssemblyReference) = originalReferenceToResolution.TryFind assemblyReference.Text - /// This doesn't need to be cancellable, it is only used by F# Interactive - member _.TryFindByExactILAssemblyRef (ctok, assemblyRef) = + /// Only used by F# Interactive + member _.TryFindByExactILAssemblyRef (assemblyRef) = results |> List.tryFind (fun ar-> - let r = ar.GetILAssemblyRef(ctok, tcConfig.reduceMemoryUsage, tcConfig.tryGetMetadataSnapshot) |> Cancellable.runWithoutCancellation + let r = ar.GetILAssemblyRef(tcConfig.reduceMemoryUsage, tcConfig.tryGetMetadataSnapshot) r = assemblyRef) - /// This doesn't need to be cancellable, it is only used by F# Interactive - member _.TryFindBySimpleAssemblyName (ctok, simpleAssemName) = + /// Only used by F# Interactive + member _.TryFindBySimpleAssemblyName (simpleAssemName) = results |> List.tryFind (fun ar-> - let r = ar.GetILAssemblyRef(ctok, tcConfig.reduceMemoryUsage, tcConfig.tryGetMetadataSnapshot) |> Cancellable.runWithoutCancellation + let r = ar.GetILAssemblyRef(tcConfig.reduceMemoryUsage, tcConfig.tryGetMetadataSnapshot) r.Name = simpleAssemName) member _.TryFindByResolvedPath nm = resolvedPathToResolution.TryFind nm member _.TryFindByOriginalReferenceText nm = originalReferenceToResolution.TryFind nm - static member ResolveAssemblyReferences (ctok, tcConfig: TcConfig, assemblyList: AssemblyReference list, knownUnresolved: UnresolvedAssemblyReference list) : TcAssemblyResolutions = + static member ResolveAssemblyReferences (tcConfig: TcConfig, assemblyList: AssemblyReference list, knownUnresolved: UnresolvedAssemblyReference list) : TcAssemblyResolutions = let resolved, unresolved = if tcConfig.useSimpleResolution then let resolutions = @@ -569,7 +559,7 @@ type TcAssemblyResolutions(tcConfig: TcConfig, results: AssemblyResolution list, let failures = resolutions |> List.choose (function Choice2Of2 x -> Some (UnresolvedAssemblyReference(x.Text, [x])) | _ -> None) successes, failures else - RequireCompilationThread ctok // we don't want to do assembly resolution concurrently, we assume MSBuild doesn't handle this + // we don't want to do assembly resolution concurrently, we assume MSBuild doesn't handle this TcConfig.TryResolveLibsUsingMSBuildRules (tcConfig, assemblyList, rangeStartup, ResolveAssemblyReferenceMode.ReportErrors) TcAssemblyResolutions(tcConfig, resolved, unresolved @ knownUnresolved) @@ -603,46 +593,47 @@ type TcAssemblyResolutions(tcConfig: TcConfig, results: AssemblyResolution list, yield! tcConfig.referencedDLLs ] - static member SplitNonFoundationalResolutions (ctok, tcConfig: TcConfig) = + static member SplitNonFoundationalResolutions (tcConfig: TcConfig) = let assemblyList = TcAssemblyResolutions.GetAllDllReferences tcConfig - let resolutions = TcAssemblyResolutions.ResolveAssemblyReferences (ctok, tcConfig, assemblyList, tcConfig.knownUnresolvedReferences) + let resolutions = TcAssemblyResolutions.ResolveAssemblyReferences (tcConfig, assemblyList, tcConfig.knownUnresolvedReferences) let frameworkDLLs, nonFrameworkReferences = resolutions.GetAssemblyResolutions() |> List.partition (fun r -> r.sysdir) let unresolved = resolutions.GetUnresolvedReferences() #if DEBUG let mutable itFailed = false let addedText = "\nIf you want to debug this right now, attach a debugger, and put a breakpoint in 'CompileOps.fs' near the text '!itFailed', and you can re-step through the assembly resolution logic." - unresolved - |> List.iter (fun (UnresolvedAssemblyReference(referenceText, _ranges)) -> + + for (UnresolvedAssemblyReference(referenceText, _ranges)) in unresolved do if referenceText.Contains("mscorlib") then System.Diagnostics.Debug.Assert(false, sprintf "whoops, did not resolve mscorlib: '%s'%s" referenceText addedText) - itFailed <- true) - frameworkDLLs - |> List.iter (fun x -> + itFailed <- true + + for x in frameworkDLLs do if not(FileSystem.IsPathRootedShim(x.resolvedPath)) then System.Diagnostics.Debug.Assert(false, sprintf "frameworkDLL should be absolute path: '%s'%s" x.resolvedPath addedText) - itFailed <- true) - nonFrameworkReferences - |> List.iter (fun x -> + itFailed <- true + + for x in nonFrameworkReferences do if not(FileSystem.IsPathRootedShim(x.resolvedPath)) then System.Diagnostics.Debug.Assert(false, sprintf "nonFrameworkReference should be absolute path: '%s'%s" x.resolvedPath addedText) - itFailed <- true) + itFailed <- true + if itFailed then // idea is, put a breakpoint here and then step through let assemblyList = TcAssemblyResolutions.GetAllDllReferences tcConfig - let resolutions = TcAssemblyResolutions.ResolveAssemblyReferences (ctok, tcConfig, assemblyList, []) + let resolutions = TcAssemblyResolutions.ResolveAssemblyReferences (tcConfig, assemblyList, []) let _frameworkDLLs, _nonFrameworkReferences = resolutions.GetAssemblyResolutions() |> List.partition (fun r -> r.sysdir) () #endif frameworkDLLs, nonFrameworkReferences, unresolved - static member BuildFromPriorResolutions (ctok, tcConfig: TcConfig, resolutions, knownUnresolved) = + static member BuildFromPriorResolutions (tcConfig: TcConfig, resolutions, knownUnresolved) = let references = resolutions |> List.map (fun r -> r.originalReference) - TcAssemblyResolutions.ResolveAssemblyReferences (ctok, tcConfig, references, knownUnresolved) + TcAssemblyResolutions.ResolveAssemblyReferences (tcConfig, references, knownUnresolved) - static member GetAssemblyResolutionInformation(ctok, tcConfig: TcConfig) = + static member GetAssemblyResolutionInformation(tcConfig: TcConfig) = use unwindBuildPhase = PushThreadBuildPhaseUntilUnwind BuildPhase.Parameter let assemblyList = TcAssemblyResolutions.GetAllDllReferences tcConfig - let resolutions = TcAssemblyResolutions.ResolveAssemblyReferences (ctok, tcConfig, assemblyList, []) + let resolutions = TcAssemblyResolutions.ResolveAssemblyReferences (tcConfig, assemblyList, []) resolutions.GetAssemblyResolutions(), resolutions.GetUnresolvedReferences() //---------------------------------------------------------------------------- @@ -776,17 +767,31 @@ type RawFSharpAssemblyData (ilModule: ILModuleDef, ilAssemblyRefs) = //-------------------------------------------------------------------------- [] -type TcImportsSafeDisposal(disposeActions: ResizeArray unit>,disposeTypeProviderActions: ResizeArray unit>) = +type TcImportsSafeDisposal(tciLock: TcImportsLock, disposeActions: ResizeArray unit>,disposeTypeProviderActions: ResizeArray unit>) = let mutable isDisposed = false let dispose () = + tciLock.AcquireLock (fun tcitok -> + + RequireTcImportsLock (tcitok, isDisposed) + RequireTcImportsLock (tcitok, disposeTypeProviderActions) + RequireTcImportsLock (tcitok, disposeActions) + // disposing deliberately only closes this tcImports, not the ones up the chain isDisposed <- true if verbose then dprintf "disposing of TcImports, %d binaries\n" disposeActions.Count - for action in disposeTypeProviderActions do action() - for action in disposeActions do action() + + let actions1 = disposeTypeProviderActions |> Seq.toArray + let actions2 = disposeActions |> Seq.toArray + + disposeTypeProviderActions.Clear() + disposeActions.Clear() + + for action in actions1 do action() + for action in actions2 do action() + ) override _.Finalize() = dispose () @@ -809,10 +814,12 @@ type TcImportsDllInfoHack = FileName: string } -and TcImportsWeakHack (tcImports: WeakReference) = +and TcImportsWeakHack (tciLock: TcImportsLock, tcImports: WeakReference) = let mutable dllInfos: TcImportsDllInfoHack list = [] member _.SetDllInfos (value: ImportedBinary list) = + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, dllInfos) dllInfos <- value |> List.map (fun x -> { FileName = x.FileName }) member _.Base: TcImportsWeakHack option = @@ -840,29 +847,28 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse #endif = + let tciLock = TcImportsLock() + + //---- Start protected by tciLock ------- let mutable resolutions = initialResolutions - let mutable importsBase: TcImports option = importsBase let mutable dllInfos: ImportedBinary list = [] let mutable dllTable: NameMap = NameMap.empty let mutable ccuInfos: ImportedAssembly list = [] let mutable ccuTable: NameMap = NameMap.empty - - /// ccuThunks is a ConcurrentDictionary thus threadsafe - /// the key is a ccuThunk object, the value is a (unit->unit) func that when executed - /// the func is used to fix up the func and operates on data captured at the time the func is created. - /// func() is captured during phase2() of RegisterAndPrepareToImportReferencedDll(..) and PrepareToImportReferencedFSharpAssembly ( .. ) - let mutable ccuThunks = new ConcurrentDictionary unit)>() - + let mutable ccuThunks = ResizeArray unit)>() let disposeActions = ResizeArray() - let mutable disposed = false - let mutable tcGlobals = None let disposeTypeProviderActions = ResizeArray() + #if !NO_EXTENSIONTYPING - let mutable generatedTypeRoots = new System.Collections.Generic.Dictionary() - let mutable tcImportsWeak = TcImportsWeakHack (WeakReference<_> this) + let mutable generatedTypeRoots = new Dictionary() + let tcImportsWeak = TcImportsWeakHack (tciLock, WeakReference<_> this) #endif - let disposal = new TcImportsSafeDisposal(disposeActions, disposeTypeProviderActions) + let disposal = new TcImportsSafeDisposal(tciLock, disposeActions, disposeTypeProviderActions) + //---- End protected by tciLock ------- + + let mutable disposed = false // this doesn't need locking, it's only for debugging + let mutable tcGlobals = None // this doesn't need locking, it's set during construction of the TcImports let CheckDisposed() = if disposed then assert false @@ -871,24 +877,22 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse CheckDisposed() (disposal :> IDisposable).Dispose() - // This is used to fixe up unresolved ccuThunks that were created during assembly import. - // the ccuThunks dictionary is a ConcurrentDictionary and thus threadsafe. - // Algorithm: // Get a snapshot of the current unFixedUp ccuThunks. // for each of those thunks, remove them from the dictionary, so any parallel threads can't do this work // If it successfully removed it from the dictionary then do the fixup // If the thunk remains unresolved add it back to the ccuThunks dictionary for further processing // If not then move on to the next thunk let fixupOrphanCcus () = - let keys = ccuThunks.Keys - for ccuThunk in keys do - match ccuThunks.TryRemove(ccuThunk) with - | true, func -> + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, ccuThunks) + let contents = ccuThunks |> Seq.toArray + let unsuccessful = + [ for (ccuThunk, func) in contents do if ccuThunk.IsUnresolvedReference then func() if ccuThunk.IsUnresolvedReference then - ccuThunks.TryAdd(ccuThunk, func) |> ignore - | _ -> () + yield (ccuThunk, func) ] + ccuThunks <- ResizeArray (unsuccessful) let availableToOptionalCcu = function | ResolvedCcu ccu -> Some ccu @@ -909,16 +913,20 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse | None -> false member internal tcImports.Base = - CheckDisposed() - importsBase + CheckDisposed() + importsBase member tcImports.CcuTable = - CheckDisposed() - ccuTable + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, ccuTable) + CheckDisposed() + ccuTable member tcImports.DllTable = - CheckDisposed() - dllTable + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, dllTable) + CheckDisposed() + dllTable #if !NO_EXTENSIONTYPING member tcImports.Weak = @@ -927,13 +935,19 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse #endif member tcImports.RegisterCcu ccuInfo = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, ccuInfos) + RequireTcImportsLock(tcitok, ccuTable) ccuInfos <- ccuInfos ++ ccuInfo // Assembly Ref Resolution: remove this use of ccu.AssemblyName ccuTable <- NameMap.add (ccuInfo.FSharpViewOfMetadata.AssemblyName) ccuInfo ccuTable member tcImports.RegisterDll dllInfo = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, dllInfos) + RequireTcImportsLock(tcitok, dllTable) dllInfos <- dllInfos ++ dllInfo #if !NO_EXTENSIONTYPING tcImportsWeak.SetDllInfos dllInfos @@ -941,13 +955,17 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse dllTable <- NameMap.add (getNameOfScopeRef dllInfo.ILScopeRef) dllInfo dllTable member tcImports.GetDllInfos() : ImportedBinary list = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, dllInfos) match importsBase with - | Some importsBase-> importsBase.GetDllInfos() @ dllInfos + | Some importsBase -> importsBase.GetDllInfos() @ dllInfos | None -> dllInfos member tcImports.AllAssemblyResolutions() = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, resolutions) let ars = resolutions.GetAssemblyResolutions() match importsBase with | Some importsBase-> importsBase.AllAssemblyResolutions() @ ars @@ -974,13 +992,17 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse | None -> error(Error(FSComp.SR.buildCouldNotResolveAssembly assemblyName, m)) member tcImports.GetImportedAssemblies() = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, ccuInfos) match importsBase with - | Some importsBase-> List.append (importsBase.GetImportedAssemblies()) ccuInfos + | Some importsBase -> List.append (importsBase.GetImportedAssemblies()) ccuInfos | None -> ccuInfos member tcImports.GetCcusExcludingBase() = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, ccuInfos) ccuInfos |> List.map (fun x -> x.FSharpViewOfMetadata) member tcImports.GetCcusInDeclOrder() = @@ -1110,15 +1132,19 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse true, dllinfo.ProviderGeneratedStaticLinkMap member tcImports.RecordGeneratedTypeRoot root = + tciLock.AcquireLock <| fun tcitok -> // checking if given ProviderGeneratedType was already recorded before (probably for another set of static parameters) let (ProviderGeneratedType(_, ilTyRef, _)) = root let index = + RequireTcImportsLock(tcitok, generatedTypeRoots) match generatedTypeRoots.TryGetValue ilTyRef with | true, (index, _) -> index | false, _ -> generatedTypeRoots.Count generatedTypeRoots.[ilTyRef] <- (index, root) member tcImports.ProviderGeneratedTypeRoots = + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, generatedTypeRoots) generatedTypeRoots.Values |> Seq.sortBy fst |> Seq.map snd @@ -1126,7 +1152,9 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse #endif member private tcImports.AttachDisposeAction action = + tciLock.AcquireLock <| fun tcitok -> CheckDisposed() + RequireTcImportsLock(tcitok, disposeActions) disposeActions.Add action #if !NO_EXTENSIONTYPING @@ -1468,6 +1496,7 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse #endif FSharpOptimizationData = notlazy None } tcImports.RegisterCcu ccuinfo + let phase2 () = #if !NO_EXTENSIONTYPING ccuinfo.TypeProviders <- tcImports.ImportTypeProviderExtensions (ctok, tcConfig, filename, ilScopeRef, ilModule.ManifestOfAssembly.CustomAttrs.AsList, ccu.Contents, invalidateCcu, m) @@ -1538,9 +1567,12 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse let fixupThunk () = data.OptionalFixup(fun nm -> availableToOptionalCcu(tcImports.FindCcu(ctok, m, nm, lookupOnly=false))) // Make a note of all ccuThunks that may still need to be fixed up when other dlls are loaded - for ccuThunk in data.FixupThunks do - if ccuThunk.IsUnresolvedReference then - ccuThunks.TryAdd(ccuThunk, fun () -> fixupThunk () |> ignore) |> ignore + tciLock.AcquireLock (fun tcitok -> + RequireTcImportsLock(tcitok, ccuThunks) + for ccuThunk in data.FixupThunks do + if ccuThunk.IsUnresolvedReference then + ccuThunks.Add(ccuThunk, fun () -> fixupThunk () |> ignore) |> ignore + ) if verbose then dprintf "found optimization data for CCU %s\n" ccuName Some (fixupThunk ())) @@ -1579,7 +1611,9 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse fixupThunk() for ccuThunk in data.FixupThunks do if ccuThunk.IsUnresolvedReference then - ccuThunks.TryAdd(ccuThunk, fixupThunk) |> ignore + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, ccuThunks) + ccuThunks.Add(ccuThunk, fixupThunk) |> ignore ) #if !NO_EXTENSIONTYPING ccuRawDataAndInfos |> List.iter (fun (_, _, phase2) -> phase2()) @@ -1588,16 +1622,18 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse phase2 // NOTE: When used in the Language Service this can cause the transitive checking of projects. Hence it must be cancellable. - member tcImports.TryRegisterAndPrepareToImportReferencedDll (ctok, r: AssemblyResolution) : Cancellable<(_ * (unit -> AvailableImportedAssembly list)) option> = - cancellable { + member tcImports.TryRegisterAndPrepareToImportReferencedDll (ctok, r: AssemblyResolution) : NodeCode<(_ * (unit -> AvailableImportedAssembly list)) option> = + node { CheckDisposed() let m = r.originalReference.Range let filename = r.resolvedPath let! contentsOpt = - cancellable { + node { match r.ProjectReference with - | Some ilb -> return! ilb.EvaluateRawContents ctok - | None -> return None + | Some ilb -> + return! ilb.EvaluateRawContents() + | None -> + return None } // If we have a project reference but did not get any valid contents, @@ -1648,19 +1684,23 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse // NOTE: When used in the Language Service this can cause the transitive checking of projects. Hence it must be cancellable. member tcImports.RegisterAndImportReferencedAssemblies (ctok, nms: AssemblyResolution list) = - cancellable { + node { CheckDisposed() + let! results = - nms |> Cancellable.each (fun nm -> - cancellable { - try - return! tcImports.TryRegisterAndPrepareToImportReferencedDll (ctok, nm) - with e -> - errorR(Error(FSComp.SR.buildProblemReadingAssembly(nm.resolvedPath, e.Message), nm.originalReference.Range)) - return None - }) - - let dllinfos, phase2s = results |> List.choose id |> List.unzip + nms + |> List.map (fun nm -> + node { + try + return! tcImports.TryRegisterAndPrepareToImportReferencedDll (ctok, nm) + with e -> + errorR(Error(FSComp.SR.buildProblemReadingAssembly(nm.resolvedPath, e.Message), nm.originalReference.Range)) + return None + } + ) + |> NodeCode.Sequential + + let dllinfos, phase2s = results |> Array.choose id |> List.ofArray |> List.unzip fixupOrphanCcus() let ccuinfos = (List.collect (fun phase2 -> phase2()) phase2s) return dllinfos, ccuinfos @@ -1680,7 +1720,9 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse match foundFile with | OkResult (warns, res) -> ReportWarnings warns - tcImports.RegisterAndImportReferencedAssemblies(ctok, res) |> Cancellable.runWithoutCancellation |> ignore + tcImports.RegisterAndImportReferencedAssemblies(ctok, res) + |> NodeCode.RunImmediateWithoutCancellation + |> ignore true | ErrorResult (_warns, _err) -> // Throw away warnings and errors - this is speculative loading @@ -1699,16 +1741,23 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse | _ -> None #endif - /// This doesn't need to be cancellable, it is only used by F# Interactive - member tcImports.TryFindExistingFullyQualifiedPathBySimpleAssemblyName (ctok, simpleAssemName) : string option = - resolutions.TryFindBySimpleAssemblyName (ctok, simpleAssemName) |> Option.map (fun r -> r.resolvedPath) + /// Only used by F# Interactive + member tcImports.TryFindExistingFullyQualifiedPathBySimpleAssemblyName (simpleAssemName) : string option = + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, resolutions) + resolutions.TryFindBySimpleAssemblyName (simpleAssemName) |> Option.map (fun r -> r.resolvedPath) - /// This doesn't need to be cancellable, it is only used by F# Interactive - member tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef(ctok, assemblyRef: ILAssemblyRef) : string option = - resolutions.TryFindByExactILAssemblyRef (ctok, assemblyRef) |> Option.map (fun r -> r.resolvedPath) + /// Only used by F# Interactive + member tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef(assemblyRef: ILAssemblyRef) : string option = + tciLock.AcquireLock <| fun tcitok -> + RequireTcImportsLock(tcitok, resolutions) + resolutions.TryFindByExactILAssemblyRef (assemblyRef) |> Option.map (fun r -> r.resolvedPath) member tcImports.TryResolveAssemblyReference(ctok, assemblyReference: AssemblyReference, mode: ResolveAssemblyReferenceMode) : OperationResult = + tciLock.AcquireLock <| fun tcitok -> let tcConfig = tcConfigP.Get ctok + + RequireTcImportsLock(tcitok, resolutions) // First try to lookup via the original reference text. match resolutions.TryFindByOriginalReference assemblyReference with | Some assemblyResolution -> @@ -1759,12 +1808,12 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse // Note: This returns a TcImports object. However, framework TcImports are not currently disposed. The only reason // we dispose TcImports is because we need to dispose type providers, and type providers are never included in the framework DLL set. // If a framework set ever includes type providers, you will not have to worry about explicitly calling Dispose as the Finalizer will handle it. - static member BuildFrameworkTcImports (ctok, tcConfigP: TcConfigProvider, frameworkDLLs, nonFrameworkDLLs) = - cancellable { - + static member BuildFrameworkTcImports (tcConfigP: TcConfigProvider, frameworkDLLs, nonFrameworkDLLs) = + node { + let ctok = CompilationThreadToken() let tcConfig = tcConfigP.Get ctok - let tcResolutions = TcAssemblyResolutions.BuildFromPriorResolutions(ctok, tcConfig, frameworkDLLs, []) - let tcAltResolutions = TcAssemblyResolutions.BuildFromPriorResolutions(ctok, tcConfig, nonFrameworkDLLs, []) + let tcResolutions = TcAssemblyResolutions.BuildFromPriorResolutions(tcConfig, frameworkDLLs, []) + let tcAltResolutions = TcAssemblyResolutions.BuildFromPriorResolutions(tcConfig, nonFrameworkDLLs, []) let frameworkTcImports = new TcImports(tcConfigP, tcResolutions, None, None) @@ -1819,12 +1868,12 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse else None) - let fslibCcu, fsharpCoreAssemblyScopeRef = - if tcConfig.compilingFslib then - // When compiling FSharp.Core.dll, the fslibCcu reference to FSharp.Core.dll is a delayed ccu thunk fixed up during type checking - CcuThunk.CreateDelayed getFSharpCoreLibraryName, ILScopeRef.Local - else - let fslibCcuInfo = + let! fslibCcu, fsharpCoreAssemblyScopeRef = + node { + if tcConfig.compilingFslib then + // When compiling FSharp.Core.dll, the fslibCcu reference to FSharp.Core.dll is a delayed ccu thunk fixed up during type checking + return CcuThunk.CreateDelayed getFSharpCoreLibraryName, ILScopeRef.Local + else let coreLibraryReference = tcConfig.CoreLibraryDllReference() let resolvedAssemblyRef = @@ -1838,13 +1887,13 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse match resolvedAssemblyRef with | Some coreLibraryResolution -> - match frameworkTcImports.RegisterAndImportReferencedAssemblies(ctok, [coreLibraryResolution]) |> Cancellable.runWithoutCancellation with - | (_, [ResolvedImportedAssembly fslibCcuInfo ]) -> fslibCcuInfo + match! frameworkTcImports.RegisterAndImportReferencedAssemblies(ctok, [coreLibraryResolution]) with + | (_, [ResolvedImportedAssembly fslibCcuInfo ]) -> return fslibCcuInfo.FSharpViewOfMetadata, fslibCcuInfo.ILScopeRef | _ -> - error(InternalError("BuildFrameworkTcImports: no successful import of "+coreLibraryResolution.resolvedPath, coreLibraryResolution.originalReference.Range)) + return error(InternalError("BuildFrameworkTcImports: no successful import of "+coreLibraryResolution.resolvedPath, coreLibraryResolution.originalReference.Range)) | None -> - error(InternalError(sprintf "BuildFrameworkTcImports: no resolution of '%s'" coreLibraryReference.Text, rangeStartup)) - fslibCcuInfo.FSharpViewOfMetadata, fslibCcuInfo.ILScopeRef + return error(InternalError(sprintf "BuildFrameworkTcImports: no resolution of '%s'" coreLibraryReference.Text, rangeStartup)) + } // Load the rest of the framework DLLs all at once (they may be mutually recursive) let! _assemblies = frameworkTcImports.RegisterAndImportReferencedAssemblies (ctok, resolvedAssemblies) @@ -1882,12 +1931,13 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse |> List.iter reportAssemblyNotResolved static member BuildNonFrameworkTcImports - (ctok, tcConfigP: TcConfigProvider, baseTcImports, + (tcConfigP: TcConfigProvider, baseTcImports, nonFrameworkReferences, knownUnresolved, dependencyProvider) = - cancellable { + node { + let ctok = CompilationThreadToken() let tcConfig = tcConfigP.Get ctok - let tcResolutions = TcAssemblyResolutions.BuildFromPriorResolutions(ctok, tcConfig, nonFrameworkReferences, knownUnresolved) + let tcResolutions = TcAssemblyResolutions.BuildFromPriorResolutions(tcConfig, nonFrameworkReferences, knownUnresolved) let references = tcResolutions.GetAssemblyResolutions() let tcImports = new TcImports(tcConfigP, tcResolutions, Some baseTcImports, Some dependencyProvider) let! _assemblies = tcImports.RegisterAndImportReferencedAssemblies(ctok, references) @@ -1895,13 +1945,13 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse return tcImports } - static member BuildTcImports(ctok, tcConfigP: TcConfigProvider, dependencyProvider) = - cancellable { + static member BuildTcImports(tcConfigP: TcConfigProvider, dependencyProvider) = + node { + let ctok = CompilationThreadToken() let tcConfig = tcConfigP.Get ctok - //let foundationalTcImports, tcGlobals = TcImports.BuildFoundationalTcImports tcConfigP - let frameworkDLLs, nonFrameworkReferences, knownUnresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(ctok, tcConfig) - let! tcGlobals, frameworkTcImports = TcImports.BuildFrameworkTcImports (ctok, tcConfigP, frameworkDLLs, nonFrameworkReferences) - let! tcImports = TcImports.BuildNonFrameworkTcImports(ctok, tcConfigP, frameworkTcImports, nonFrameworkReferences, knownUnresolved, dependencyProvider) + let frameworkDLLs, nonFrameworkReferences, knownUnresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(tcConfig) + let! tcGlobals, frameworkTcImports = TcImports.BuildFrameworkTcImports (tcConfigP, frameworkDLLs, nonFrameworkReferences) + let! tcImports = TcImports.BuildNonFrameworkTcImports(tcConfigP, frameworkTcImports, nonFrameworkReferences, knownUnresolved, dependencyProvider) return tcGlobals, tcImports } @@ -1915,7 +1965,9 @@ and [] TcImports(tcConfigP: TcConfigProvider, initialResolutions: TcAsse /// Adds the reference to the tcImports and add the ccu to the type checking environment. let RequireDLL (ctok, tcImports: TcImports, tcEnv, thisAssemblyName, referenceRange, file) = let resolutions = CommitOperationResult(tcImports.TryResolveAssemblyReference(ctok, AssemblyReference(referenceRange, file, None), ResolveAssemblyReferenceMode.ReportErrors)) - let dllinfos, ccuinfos = tcImports.RegisterAndImportReferencedAssemblies(ctok, resolutions) |> Cancellable.runWithoutCancellation + let dllinfos, ccuinfos = + tcImports.RegisterAndImportReferencedAssemblies(ctok, resolutions) + |> NodeCode.RunImmediateWithoutCancellation let asms = ccuinfos |> List.map (function diff --git a/src/fsharp/CompilerImports.fsi b/src/fsharp/CompilerImports.fsi index 2538857e1f..5a840c368b 100644 --- a/src/fsharp/CompilerImports.fsi +++ b/src/fsharp/CompilerImports.fsi @@ -16,6 +16,7 @@ open FSharp.Compiler.Optimizer open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.TcGlobals +open FSharp.Compiler.BuildGraph open FSharp.Compiler.Text open FSharp.Core.CompilerServices @@ -122,11 +123,11 @@ type TcAssemblyResolutions = member GetAssemblyResolutions: unit -> AssemblyResolution list - static member SplitNonFoundationalResolutions: ctok: CompilationThreadToken * tcConfig: TcConfig -> AssemblyResolution list * AssemblyResolution list * UnresolvedAssemblyReference list + static member SplitNonFoundationalResolutions: tcConfig: TcConfig -> AssemblyResolution list * AssemblyResolution list * UnresolvedAssemblyReference list - static member BuildFromPriorResolutions: ctok: CompilationThreadToken * tcConfig: TcConfig * AssemblyResolution list * UnresolvedAssemblyReference list -> TcAssemblyResolutions + static member BuildFromPriorResolutions: tcConfig: TcConfig * AssemblyResolution list * UnresolvedAssemblyReference list -> TcAssemblyResolutions - static member GetAssemblyResolutionInformation: ctok: CompilationThreadToken * tcConfig: TcConfig -> AssemblyResolution list * UnresolvedAssemblyReference list + static member GetAssemblyResolutionInformation: tcConfig: TcConfig -> AssemblyResolution list * UnresolvedAssemblyReference list [] type RawFSharpAssemblyData = @@ -173,10 +174,10 @@ type TcImports = /// Try to find the given assembly reference by simple name. Used in magic assembly resolution. Effectively does implicit /// unification of assemblies by simple assembly name. - member TryFindExistingFullyQualifiedPathBySimpleAssemblyName: CompilationThreadToken * string -> string option + member TryFindExistingFullyQualifiedPathBySimpleAssemblyName: string -> string option /// Try to find the given assembly reference. - member TryFindExistingFullyQualifiedPathByExactAssemblyRef: CompilationThreadToken * ILAssemblyRef -> string option + member TryFindExistingFullyQualifiedPathByExactAssemblyRef: ILAssemblyRef -> string option #if !NO_EXTENSIONTYPING /// Try to find a provider-generated assembly @@ -190,26 +191,23 @@ type TcImports = member internal Base: TcImports option static member BuildFrameworkTcImports: - CompilationThreadToken * TcConfigProvider * AssemblyResolution list * AssemblyResolution list - -> Cancellable + -> NodeCode static member BuildNonFrameworkTcImports: - CompilationThreadToken * TcConfigProvider * TcImports * AssemblyResolution list * UnresolvedAssemblyReference list * DependencyProvider - -> Cancellable + -> NodeCode static member BuildTcImports: - ctok: CompilationThreadToken * tcConfigP: TcConfigProvider * dependencyProvider: DependencyProvider - -> Cancellable + -> NodeCode /// Process #r in F# Interactive. /// Adds the reference to the tcImports and add the ccu to the type checking environment. diff --git a/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj b/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj index d9df9c7eae..cfbae17b9a 100644 --- a/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj +++ b/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj @@ -701,6 +701,12 @@ Driver\DependencyProvider.fs + + Driver\BuildGraph.fsi + + + Driver\BuildGraph.fs + Driver\CompilerConfig.fsi @@ -799,12 +805,6 @@ Symbols/SymbolPatterns.fs - - Service/Reactor.fsi - - - Service/Reactor.fs - diff --git a/src/fsharp/FxResolver.fs b/src/fsharp/FxResolver.fs index 9a1eb6a920..dc8cd59a31 100644 --- a/src/fsharp/FxResolver.fs +++ b/src/fsharp/FxResolver.fs @@ -12,11 +12,17 @@ open System.IO open System.Reflection open System.Runtime.InteropServices open Internal.Utilities.FSharpEnvironment +open Internal.Utilities.Library open FSharp.Compiler.AbstractIL.ILBinaryReader open FSharp.Compiler.ErrorLogger open FSharp.Compiler.Text open FSharp.Compiler.IO +type internal FxResolverLockToken() = + interface LockToken + +type internal FxResolverLock = Lock + /// Resolves the references for a chosen or currently-executing framework, for /// - script execution /// - script editing @@ -26,6 +32,10 @@ open FSharp.Compiler.IO /// - default references for fsi.exe type internal FxResolver(assumeDotNetFramework: bool, projectDir: string, useSdkRefs: bool, isInteractive: bool, rangeForErrors: range, sdkDirOverride: string option) = + let fxlock = FxResolverLock() + + static let RequireFxResolverLock (_fxtok: FxResolverLockToken, _thingProtected: 'T) = () + /// We only try once for each directory (cleared on solution unload) to prevent conditions where /// we repeatedly try to run dotnet.exe on every keystroke for a script static let desiredDotNetSdkVersionForDirectoryCache = ConcurrentDictionary>() @@ -763,16 +773,24 @@ type internal FxResolver(assumeDotNetFramework: bool, projectDir: string, useSdk member _.GetSystemAssemblies() = systemAssemblies member _.IsInReferenceAssemblyPackDirectory filename = + fxlock.AcquireLock <| fun fxtok -> + RequireFxResolverLock(fxtok, "assuming all member require lock") + match tryGetNetCoreRefsPackDirectoryRoot() |> replayWarnings with | _, Some root -> let path = Path.GetDirectoryName(filename) path.StartsWith(root, StringComparison.OrdinalIgnoreCase) | _ -> false - member _.TryGetSdkDir() = tryGetSdkDir() |> replayWarnings + member _.TryGetSdkDir() = + fxlock.AcquireLock <| fun fxtok -> + RequireFxResolverLock(fxtok, "assuming all member require lock") + tryGetSdkDir() |> replayWarnings /// Gets the selected target framework moniker, e.g netcore3.0, net472, and the running rid of the current machine member _.GetTfmAndRid() = + fxlock.AcquireLock <| fun fxtok -> + RequireFxResolverLock(fxtok, "assuming all member require lock") // Interactive processes read their own configuration to find the running tfm let tfm = @@ -819,12 +837,20 @@ type internal FxResolver(assumeDotNetFramework: bool, projectDir: string, useSdk static member ClearStaticCaches() = desiredDotNetSdkVersionForDirectoryCache.Clear() - member _.GetFrameworkRefsPackDirectory() = tryGetSdkRefsPackDirectory() |> replayWarnings + member _.GetFrameworkRefsPackDirectory() = + fxlock.AcquireLock <| fun fxtok -> + RequireFxResolverLock(fxtok, "assuming all member require lock") + tryGetSdkRefsPackDirectory() |> replayWarnings - member _.TryGetDesiredDotNetSdkVersionForDirectory() = tryGetDesiredDotNetSdkVersionForDirectoryInfo() + member _.TryGetDesiredDotNetSdkVersionForDirectory() = + fxlock.AcquireLock <| fun fxtok -> + RequireFxResolverLock(fxtok, "assuming all member require lock") + tryGetDesiredDotNetSdkVersionForDirectoryInfo() // The set of references entered into the TcConfigBuilder for scripts prior to computing the load closure. member _.GetDefaultReferences (useFsiAuxLib) = + fxlock.AcquireLock <| fun fxtok -> + RequireFxResolverLock(fxtok, "assuming all member require lock") let defaultReferences = if assumeDotNetFramework then getDotNetFrameworkDefaultReferences useFsiAuxLib, assumeDotNetFramework diff --git a/src/fsharp/ParseAndCheckInputs.fs b/src/fsharp/ParseAndCheckInputs.fs index 33b2278c72..ffe1bfe4a9 100644 --- a/src/fsharp/ParseAndCheckInputs.fs +++ b/src/fsharp/ParseAndCheckInputs.fs @@ -755,9 +755,9 @@ let GetInitialTcState(m, ccuName, tcConfig: TcConfig, tcGlobals, tcImports: TcIm tcsCcuSig = Construct.NewEmptyModuleOrNamespaceType Namespace } /// Typecheck a single file (or interactive entry into F# Interactive) -let TypeCheckOneInputEventually (checkForErrors, tcConfig: TcConfig, tcImports: TcImports, tcGlobals, prefixPathOpt, tcSink, tcState: TcState, inp: ParsedInput, skipImplIfSigExists: bool) = +let TypeCheckOneInput (checkForErrors, tcConfig: TcConfig, tcImports: TcImports, tcGlobals, prefixPathOpt, tcSink, tcState: TcState, inp: ParsedInput, skipImplIfSigExists: bool) = - eventually { + cancellable { try CheckSimulateException tcConfig @@ -826,7 +826,7 @@ let TypeCheckOneInputEventually (checkForErrors, tcConfig: TcConfig, tcImports: let dummyImplFile = TypedImplFile.TImplFile(qualNameOfFile, [], dummyExpr, false, false, StampMap []) (EmptyTopAttrs, dummyImplFile, Unchecked.defaultof<_>, tcImplEnv, false) - |> Eventually.Done + |> Cancellable.ret else TypeCheckOneImplFile (tcGlobals, tcState.tcsNiceNameGen, amap, tcState.tcsCcu, checkForErrors, conditionalDefines, tcSink, tcConfig.internalTestSpanStackReferring) tcImplEnv rootSigOpt file @@ -876,17 +876,14 @@ let TypeCheckOneInputEventually (checkForErrors, tcConfig: TcConfig, tcImports: } /// Typecheck a single file (or interactive entry into F# Interactive) -let TypeCheckOneInput (ctok, checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt) tcState inp = +let TypeCheckOneInputEntry (ctok, checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt) tcState inp = // 'use' ensures that the warning handler is restored at the end use unwindEL = PushErrorLoggerPhaseUntilUnwind(fun oldLogger -> GetErrorLoggerFilteringByScopedPragmas(false, GetScopedPragmasForInput inp, oldLogger) ) use unwindBP = PushThreadBuildPhaseUntilUnwind BuildPhase.TypeCheck RequireCompilationThread ctok - TypeCheckOneInputEventually (checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt, TcResultsSink.NoSink, tcState, inp, false) - |> Eventually.force CancellationToken.None - |> function - | ValueOrCancelled.Value v -> v - | ValueOrCancelled.Cancelled ce -> raise ce // this condition is unexpected, since CancellationToken.None was passed + TypeCheckOneInput (checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt, TcResultsSink.NoSink, tcState, inp, false) + |> Cancellable.runWithoutCancellation /// Finish checking multiple files (or one interactive entry into F# Interactive) let TypeCheckMultipleInputsFinish(results, tcState: TcState) = @@ -897,29 +894,30 @@ let TypeCheckMultipleInputsFinish(results, tcState: TcState) = let tcEnvAtEndOfLastFile = (match tcEnvsAtEndFile with h :: _ -> h | _ -> tcState.TcEnvFromSignatures) (tcEnvAtEndOfLastFile, topAttrs, implFiles, ccuSigsForFiles), tcState -let TypeCheckOneInputAndFinishEventually(checkForErrors, tcConfig: TcConfig, tcImports, tcGlobals, prefixPathOpt, tcSink, tcState, input) = - eventually { +let TypeCheckOneInputAndFinish(checkForErrors, tcConfig: TcConfig, tcImports, tcGlobals, prefixPathOpt, tcSink, tcState, input) = + cancellable { Logger.LogBlockStart LogCompilerFunctionId.CompileOps_TypeCheckOneInputAndFinishEventually - let! results, tcState = TypeCheckOneInputEventually(checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt, tcSink, tcState, input, false) + let! results, tcState = TypeCheckOneInput(checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt, tcSink, tcState, input, false) let result = TypeCheckMultipleInputsFinish([results], tcState) Logger.LogBlockStop LogCompilerFunctionId.CompileOps_TypeCheckOneInputAndFinishEventually return result } let TypeCheckClosedInputSetFinish (declaredImpls: TypedImplFile list, tcState) = - // Publish the latest contents to the CCU - tcState.tcsCcu.Deref.Contents <- Construct.NewCcuContents ILScopeRef.Local range0 tcState.tcsCcu.AssemblyName tcState.tcsCcuSig + // Latest contents to the CCU + let ccuContents = Construct.NewCcuContents ILScopeRef.Local range0 tcState.tcsCcu.AssemblyName tcState.tcsCcuSig // Check all interfaces have implementations tcState.tcsRootSigs |> Zmap.iter (fun qualNameOfFile _ -> if not (Zset.contains qualNameOfFile tcState.tcsRootImpls) then errorR(Error(FSComp.SR.buildSignatureWithoutImplementation(qualNameOfFile.Text), qualNameOfFile.Range))) - tcState, declaredImpls + tcState, declaredImpls, ccuContents let TypeCheckClosedInputSet (ctok, checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt, tcState, inputs) = // tcEnvAtEndOfLastFile is the environment required by fsi.exe when incrementally adding definitions - let results, tcState = (tcState, inputs) ||> List.mapFold (TypeCheckOneInput (ctok, checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt)) + let results, tcState = (tcState, inputs) ||> List.mapFold (TypeCheckOneInputEntry (ctok, checkForErrors, tcConfig, tcImports, tcGlobals, prefixPathOpt)) let (tcEnvAtEndOfLastFile, topAttrs, implFiles, _), tcState = TypeCheckMultipleInputsFinish(results, tcState) - let tcState, declaredImpls = TypeCheckClosedInputSetFinish (implFiles, tcState) + let tcState, declaredImpls, ccuContents = TypeCheckClosedInputSetFinish (implFiles, tcState) + tcState.Ccu.Deref.Contents <- ccuContents tcState, topAttrs, declaredImpls, tcEnvAtEndOfLastFile diff --git a/src/fsharp/ParseAndCheckInputs.fsi b/src/fsharp/ParseAndCheckInputs.fsi index c7a68dd894..207d41c878 100644 --- a/src/fsharp/ParseAndCheckInputs.fsi +++ b/src/fsharp/ParseAndCheckInputs.fsi @@ -83,7 +83,7 @@ val GetInitialTcState: range * string * TcConfig * TcGlobals * TcImports * NiceNameGenerator * TcEnv -> TcState /// Check one input, returned as an Eventually computation -val TypeCheckOneInputEventually : +val TypeCheckOneInput: checkForErrors:(unit -> bool) * TcConfig * TcImports * @@ -93,7 +93,7 @@ val TypeCheckOneInputEventually : TcState * ParsedInput * skipImplIfSigExists: bool - -> Eventually<(TcEnv * TopAttribs * TypedImplFile option * ModuleOrNamespaceType) * TcState> + -> Cancellable<(TcEnv * TopAttribs * TypedImplFile option * ModuleOrNamespaceType) * TcState> /// Finish the checking of multiple inputs val TypeCheckMultipleInputsFinish: (TcEnv * TopAttribs * 'T option * 'U) list * TcState -> (TcEnv * TopAttribs * 'T list * 'U list) * TcState @@ -102,7 +102,7 @@ val TypeCheckMultipleInputsFinish: (TcEnv * TopAttribs * 'T option * 'U) list * val TypeCheckClosedInputSetFinish: TypedImplFile list * TcState - -> TcState * TypedImplFile list + -> TcState * TypedImplFile list * ModuleOrNamespace /// Check a closed set of inputs val TypeCheckClosedInputSet: @@ -116,7 +116,7 @@ val TypeCheckClosedInputSet: -> TcState * TopAttribs * TypedImplFile list * TcEnv /// Check a single input and finish the checking -val TypeCheckOneInputAndFinishEventually : +val TypeCheckOneInputAndFinish : checkForErrors: (unit -> bool) * TcConfig * TcImports * @@ -125,7 +125,7 @@ val TypeCheckOneInputAndFinishEventually : NameResolution.TcResultsSink * TcState * ParsedInput - -> Eventually<(TcEnv * TopAttribs * TypedImplFile list * ModuleOrNamespaceType list) * TcState> + -> Cancellable<(TcEnv * TopAttribs * TypedImplFile list * ModuleOrNamespaceType list) * TcState> val GetScopedPragmasForInput: input: ParsedInput -> ScopedPragma list diff --git a/src/fsharp/ScriptClosure.fs b/src/fsharp/ScriptClosure.fs index c1907a5f14..c7fa44a6b2 100644 --- a/src/fsharp/ScriptClosure.fs +++ b/src/fsharp/ScriptClosure.fs @@ -346,7 +346,7 @@ module ScriptPreprocessClosure = sources, tcConfig, packageReferences /// Reduce the full directive closure into LoadClosure - let GetLoadClosure(ctok, rootFilename, closureFiles, tcConfig: TcConfig, codeContext, packageReferences, earlierDiagnostics) = + let GetLoadClosure(rootFilename, closureFiles, tcConfig: TcConfig, codeContext, packageReferences, earlierDiagnostics) = // Mark the last file as isLastCompiland. let closureFiles = @@ -384,7 +384,7 @@ module ScriptPreprocessClosure = let errorLogger = CapturingErrorLogger("GetLoadClosure") use unwindEL = PushErrorLoggerPhaseUntilUnwind (fun _ -> errorLogger) - let references, unresolvedReferences = TcAssemblyResolutions.GetAssemblyResolutionInformation(ctok, tcConfig) + let references, unresolvedReferences = TcAssemblyResolutions.GetAssemblyResolutionInformation(tcConfig) let references = references |> List.map (fun ar -> ar.resolvedPath, ar) references, unresolvedReferences, errorLogger.Diagnostics @@ -426,7 +426,7 @@ module ScriptPreprocessClosure = /// Given source text, find the full load closure. Used from service.fs, when editing a script file let GetFullClosureOfScriptText - (ctok, legacyReferenceResolver, defaultFSharpBinariesDir, + (legacyReferenceResolver, defaultFSharpBinariesDir, filename, sourceText, codeContext, useSimpleResolution, useFsiAuxLib, useSdkRefs, sdkDirOverride, lexResourceManager: Lexhelp.LexResourceManager, @@ -444,7 +444,7 @@ module ScriptPreprocessClosure = useFsiAuxLib, None, applyCommandLineArgs, assumeDotNetFramework, useSdkRefs, sdkDirOverride, tryGetMetadataSnapshot, reduceMemoryUsage) - let resolutions0, _unresolvedReferences = TcAssemblyResolutions.GetAssemblyResolutionInformation(ctok, tcConfig) + let resolutions0, _unresolvedReferences = TcAssemblyResolutions.GetAssemblyResolutionInformation(tcConfig) let references0 = resolutions0 |> List.map (fun r->r.originalReference.Range, r.resolvedPath) |> Seq.distinct |> List.ofSeq references0, tcConfig.assumeDotNetFramework, scriptDefaultReferencesDiagnostics @@ -456,18 +456,18 @@ module ScriptPreprocessClosure = let closureSources = [ClosureSource(filename, range0, sourceText, true)] let closureFiles, tcConfig, packageReferences = FindClosureFiles(filename, range0, closureSources, tcConfig, codeContext, lexResourceManager, dependencyProvider) - GetLoadClosure(ctok, filename, closureFiles, tcConfig, codeContext, packageReferences, scriptDefaultReferencesDiagnostics) + GetLoadClosure(filename, closureFiles, tcConfig, codeContext, packageReferences, scriptDefaultReferencesDiagnostics) /// Given source filename, find the full load closure /// Used from fsi.fs and fsc.fs, for #load and command line let GetFullClosureOfScriptFiles - (ctok, tcConfig:TcConfig, files:(string*range) list, codeContext, + (tcConfig:TcConfig, files:(string*range) list, codeContext, lexResourceManager: Lexhelp.LexResourceManager, dependencyProvider) = let mainFile, mainFileRange = List.last files let closureSources = files |> List.collect (fun (filename, m) -> ClosureSourceOfFilename(filename, m,tcConfig.inputCodePage,true)) let closureFiles, tcConfig, packageReferences = FindClosureFiles(mainFile, mainFileRange, closureSources, tcConfig, codeContext, lexResourceManager, dependencyProvider) - GetLoadClosure(ctok, mainFile, closureFiles, tcConfig, codeContext, packageReferences, []) + GetLoadClosure(mainFile, closureFiles, tcConfig, codeContext, packageReferences, []) type LoadClosure with /// Analyze a script text and find the closure of its references. @@ -476,7 +476,7 @@ type LoadClosure with /// A temporary TcConfig is created along the way, is why this routine takes so many arguments. We want to be sure to use exactly the /// same arguments as the rest of the application. static member ComputeClosureOfScriptText - (ctok, legacyReferenceResolver, defaultFSharpBinariesDir, + (legacyReferenceResolver, defaultFSharpBinariesDir, filename: string, sourceText: ISourceText, implicitDefines, useSimpleResolution: bool, useFsiAuxLib, useSdkRefs, sdkDir, lexResourceManager: Lexhelp.LexResourceManager, applyCompilerOptions, assumeDotNetFramework, tryGetMetadataSnapshot, @@ -484,14 +484,14 @@ type LoadClosure with use unwindBuildPhase = PushThreadBuildPhaseUntilUnwind BuildPhase.Parse ScriptPreprocessClosure.GetFullClosureOfScriptText - (ctok, legacyReferenceResolver, defaultFSharpBinariesDir, filename, sourceText, + (legacyReferenceResolver, defaultFSharpBinariesDir, filename, sourceText, implicitDefines, useSimpleResolution, useFsiAuxLib, useSdkRefs, sdkDir, lexResourceManager, applyCompilerOptions, assumeDotNetFramework, tryGetMetadataSnapshot, reduceMemoryUsage, dependencyProvider) /// Analyze a set of script files and find the closure of their references. static member ComputeClosureOfScriptFiles - (ctok, tcConfig: TcConfig, files:(string*range) list, implicitDefines, + (tcConfig: TcConfig, files:(string*range) list, implicitDefines, lexResourceManager: Lexhelp.LexResourceManager, dependencyProvider) = use unwindBuildPhase = PushThreadBuildPhaseUntilUnwind BuildPhase.Parse - ScriptPreprocessClosure.GetFullClosureOfScriptFiles (ctok, tcConfig, files, implicitDefines, lexResourceManager, dependencyProvider) + ScriptPreprocessClosure.GetFullClosureOfScriptFiles (tcConfig, files, implicitDefines, lexResourceManager, dependencyProvider) diff --git a/src/fsharp/ScriptClosure.fsi b/src/fsharp/ScriptClosure.fsi index f42c5f2be2..6d717a1df7 100644 --- a/src/fsharp/ScriptClosure.fsi +++ b/src/fsharp/ScriptClosure.fsi @@ -77,7 +77,6 @@ type LoadClosure = /// A temporary TcConfig is created along the way, is why this routine takes so many arguments. We want to be sure to use exactly the /// same arguments as the rest of the application. static member ComputeClosureOfScriptText: - CompilationThreadToken * legacyReferenceResolver: LegacyReferenceResolver * defaultFSharpBinariesDir: string * filename: string * @@ -98,7 +97,6 @@ type LoadClosure = /// Analyze a set of script files and find the closure of their references. The resulting references are then added to the given TcConfig. /// Used from fsi.fs and fsc.fs, for #load and command line. static member ComputeClosureOfScriptFiles: - CompilationThreadToken * tcConfig:TcConfig * (string * range) list * implicitDefines:CodeContext * diff --git a/src/fsharp/TypedTree.fs b/src/fsharp/TypedTree.fs index 47cc041125..7b36bf377d 100644 --- a/src/fsharp/TypedTree.fs +++ b/src/fsharp/TypedTree.fs @@ -5233,6 +5233,11 @@ type CcuThunk = [] member x.DebugText = x.ToString() + /// Used at the end of comppiling an assembly to get a frozen, final stable CCU + /// for the compilation which we no longer mutate. + member x.CloneWithFinalizedContents(ccuContents) = + { x with target = { x.target with Contents = ccuContents } } + override ccu.ToString() = ccu.AssemblyName /// The result of attempting to resolve an assembly name to a full ccu. diff --git a/src/fsharp/absil/illib.fs b/src/fsharp/absil/illib.fs index fa0fb872aa..d5e20b8018 100644 --- a/src/fsharp/absil/illib.fs +++ b/src/fsharp/absil/illib.fs @@ -831,188 +831,6 @@ type CancellableBuilder() = module CancellableAutoOpens = let cancellable = CancellableBuilder() -/// Computations that can cooperatively yield -/// -/// - You can take an Eventually value and run it with Eventually.forceForTimeSlice -type Eventually<'T> = - | Done of 'T - | NotYetDone of (CancellationToken -> (Stopwatch * int64) option -> ValueOrCancelled>) - // Indicates an IDisposable should be created and disposed on each step(s) - | Delimited of (unit -> IDisposable) * Eventually<'T> - -module Eventually = - - let inline ret x = Done x - - // Convert to a Cancellable which, when run, takes all steps in the computation, - // installing Delimited resource handlers if needed. - // - // Inlined for better stack traces, because inlining erases library ranges and replaces them - // with ranges in user code. - let inline toCancellable e = - Cancellable (fun ct -> - let rec toCancellableAux e = - match e with - | Done x -> ValueOrCancelled.Value x - | Delimited (resourcef, ev2) -> - use _resource = resourcef() - toCancellableAux ev2 - | NotYetDone work -> - if ct.IsCancellationRequested then - ValueOrCancelled.Cancelled (OperationCanceledException ct) - else - match work ct None with - | ValueOrCancelled.Cancelled ce -> ValueOrCancelled.Cancelled ce - | ValueOrCancelled.Value e2 -> toCancellableAux e2 - toCancellableAux e) - - // Inlined for better stack traces, because inlining replaces ranges of the "runtime" code in the lambda - // with ranges in user code. - let inline ofCancellable (Cancellable f) = - NotYetDone (fun ct _ -> - match f ct with - | ValueOrCancelled.Cancelled ce -> ValueOrCancelled.Cancelled ce - | ValueOrCancelled.Value v -> ValueOrCancelled.Value (Done v) - ) - - let token () = NotYetDone (fun ct _ -> ValueOrCancelled.Value (Done ct)) - - let canceled () = NotYetDone (fun ct _ -> ValueOrCancelled.Cancelled (OperationCanceledException ct)) - - // Take all steps in the computation, installing Delimited resource handlers if needed - let force ct e = Cancellable.run ct (toCancellable e) - - let stepCheck (ct: CancellationToken) (swinfo: (Stopwatch * int64) option) e = - if ct.IsCancellationRequested then - match swinfo with Some (sw, _) -> sw.Stop() | _ -> () - ValueSome (ValueOrCancelled.Cancelled (OperationCanceledException(ct))) - else - match swinfo with - | Some (sw, timeShareInMilliseconds) when sw.ElapsedMilliseconds > timeShareInMilliseconds -> - sw.Stop() - ValueSome (ValueOrCancelled.Value e) - | _ -> - ValueNone - - // Take multiple steps in the computation, installing Delimited resource handlers if needed, - // until the stopwatch times out if present. - [] - let rec steps (ct: CancellationToken) (swinfo: (Stopwatch * int64) option) e = - match stepCheck ct swinfo e with - | ValueSome res -> res - | ValueNone -> - match e with - | Done _ -> ValueOrCancelled.Value e - | Delimited (resourcef, inner) -> - use _resource = resourcef() - match steps ct swinfo inner with - | ValueOrCancelled.Value (Done _ as res) -> ValueOrCancelled.Value res - | ValueOrCancelled.Value inner2 -> ValueOrCancelled.Value (Delimited (resourcef, inner2)) // maintain the Delimited until Done - | ValueOrCancelled.Cancelled ce -> ValueOrCancelled.Cancelled ce - | NotYetDone work -> - match work ct swinfo with - | ValueOrCancelled.Value e2 -> steps ct swinfo e2 - | ValueOrCancelled.Cancelled ce -> ValueOrCancelled.Cancelled ce - - // Take multiple steps in the computation, installing Delimited resource handlers if needed - let forceForTimeSlice (sw: Stopwatch) timeShareInMilliseconds (ct: CancellationToken) e = - sw.Restart() - let swinfo = Some (sw, timeShareInMilliseconds) - steps ct swinfo e - - // Inlined for better stack traces, because inlining replaces ranges of the "runtime" code in the lambda - // with ranges in user code. - let inline bind k e = - let rec bindAux e = - NotYetDone (fun ct swinfo -> - let v = steps ct swinfo e - match v with - | ValueOrCancelled.Value (Done v) -> ValueOrCancelled.Value (k v) - | ValueOrCancelled.Value e2 -> ValueOrCancelled.Value (bindAux e2) - | ValueOrCancelled.Cancelled ce -> ValueOrCancelled.Cancelled ce) - bindAux e - - // Inlined for better stack traces, because inlining replaces ranges of the "runtime" code in the lambda - // with ranges in user code. - let inline map f e = bind (f >> ret) e - - // Inlined for better stack traces, because inlining replaces ranges of the "runtime" code in the lambda - // with ranges in user code. - let inline fold f acc seq = - (Done acc, seq) ||> Seq.fold (fun acc x -> acc |> bind (fun acc -> f acc x)) - - // Inlined for better stack traces, because inlining replaces ranges of the "runtime" code in the lambda - // with ranges in user code. - let inline each f seq = - fold (fun acc x -> f x |> map (fun y -> y :: acc)) [] seq |> map List.rev - - // Catch by pushing exception handlers around all the work - let inline catch e = - let rec catchAux e = - match e with - | Done x -> Done(Result x) - | Delimited (resourcef, ev2) -> Delimited (resourcef, catchAux ev2) - | NotYetDone work -> - NotYetDone (fun ct swinfo -> - let res = try Result(work ct swinfo) with exn -> Exception exn - match res with - | Result (ValueOrCancelled.Value cont) -> ValueOrCancelled.Value (catchAux cont) - | Result (ValueOrCancelled.Cancelled ce) -> ValueOrCancelled.Cancelled ce - | Exception exn -> ValueOrCancelled.Value (Done(Exception exn))) - catchAux e - - let inline delay f = NotYetDone (fun _ct _swinfo -> ValueOrCancelled.Value (f ())) - - let inline tryFinally e compensation = - catch e - |> bind (fun res -> - compensation() - match res with - | Result v -> Eventually.Done v - | Exception e -> raise e) - - let inline tryWith e handler = - catch e - |> bind (function Result v -> Done v | Exception e -> handler e) - - let box e = map Operators.box e - - let reusing resourcef e = Eventually.Delimited(resourcef, e) - - -type EventuallyBuilder() = - - member inline _.BindReturn(e, k) = Eventually.map k e - - member inline _.Bind(e, k) = Eventually.bind k e - - member inline _.Return v = Eventually.Done v - - member inline _.ReturnFrom v = v - - member inline _.Combine(e1, e2) = e1 |> Eventually.bind (fun () -> e2) - - member inline _.TryWith(e, handler) = Eventually.tryWith e handler - - member inline _.TryFinally(e, compensation) = Eventually.tryFinally e compensation - - member inline _.Delay f = Eventually.delay f - - member inline _.Zero() = Eventually.Done () - -[] -module internal EventuallyAutoOpens = - - let eventually = new EventuallyBuilder() - -(* -let _ = eventually { return 1 } -let _ = eventually { let x = 1 in return 1 } -let _ = eventually { let! x = eventually { return 1 } in return 1 } -let _ = eventually { try return (failwith "") with _ -> return 1 } -let _ = eventually { use x = null in return 1 } -*) - /// Generates unique stamps type UniqueStampGenerator<'T when 'T : equality>() = let gate = obj () diff --git a/src/fsharp/absil/illib.fsi b/src/fsharp/absil/illib.fsi index 57e8593f73..d0fae787a5 100644 --- a/src/fsharp/absil/illib.fsi +++ b/src/fsharp/absil/illib.fsi @@ -427,104 +427,6 @@ type internal CancellableBuilder = module internal CancellableAutoOpens = val cancellable: CancellableBuilder -/// Cancellable computations that can cooperatively yield -/// -/// - You can take an Eventually value and run it with Eventually.forceForTimeSlice -type internal Eventually<'T> = - | Done of 'T - | NotYetDone of (CancellationToken -> (Stopwatch * int64) option -> ValueOrCancelled>) - | Delimited of (unit -> IDisposable) * Eventually<'T> - -module internal Eventually = - - /// Return a simple value as the result of an eventually computation - val inline ret: x:'a -> Eventually<'a> - - val box: e:Eventually<'a> -> Eventually - - // Throws away time-slicing but retains cancellation - val inline toCancellable: e:Eventually<'T> -> Cancellable<'T> - - val inline ofCancellable: Cancellable<'T> -> Eventually<'T> - - val force: ct: CancellationToken -> e:Eventually<'a> -> ValueOrCancelled<'a> - - /// Run for at most the given time slice, returning the residue computation, which may be complete. - /// If cancellation is requested then just return the computation at the point where cancellation - /// was detected. - val forceForTimeSlice: sw:Stopwatch -> timeShareInMilliseconds: int64 -> ct: CancellationToken -> e: Eventually<'a> -> ValueOrCancelled> - - /// Check if cancellation or time limit has been reached. Needed for inlined combinators - val stepCheck: ct: CancellationToken -> swinfo: (Stopwatch * int64) option -> e:'T -> ValueOrCancelled<'T> voption - - /// Take steps in the computation. Needed for inlined combinators. - [] - val steps: ct: CancellationToken -> swinfo: (Stopwatch * int64) option -> e:Eventually<'T> -> ValueOrCancelled> - - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline map: f:('a -> 'b) -> e:Eventually<'a> -> Eventually<'b> - - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline bind: k:('a -> Eventually<'b>) -> e:Eventually<'a> -> Eventually<'b> - - /// Fold a computation over a collection - // - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline fold : f:('a -> 'b -> Eventually<'a>) -> acc:'a -> seq:seq<'b> -> Eventually<'a> - - /// Map a computation over a collection - // - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline each : f:('a -> Eventually<'b>) -> seq:seq<'a> -> Eventually<'b list> - - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline catch: e:Eventually<'a> -> Eventually> - - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline delay: f:(unit -> Eventually<'T>) -> Eventually<'T> - - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline tryFinally : e:Eventually<'a> -> compensation:(unit -> unit) -> Eventually<'a> - - // Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. - val inline tryWith : e:Eventually<'a> -> handler:(System.Exception -> Eventually<'a>) -> Eventually<'a> - - /// Bind the cancellation token associated with the computation - val token: unit -> Eventually - - /// Represents a canceled computation - val canceled: unit -> Eventually<'a> - - /// Create the resource and install it on the stack each time the Eventually is restarted - val reusing: resourcef: (unit -> IDisposable) -> e:Eventually<'T> -> Eventually<'T> - -[] -// Inlined for better stack traces, replacing ranges of the "runtime" code in the lambdas with ranges in user code. -type internal EventuallyBuilder = - - member inline BindReturn: e:Eventually<'g> * k:('g -> 'h) -> Eventually<'h> - - member inline Bind: e:Eventually<'g> * k:('g -> Eventually<'h>) -> Eventually<'h> - - member inline Combine: e1:Eventually * e2:Eventually<'d> -> Eventually<'d> - - member inline Delay: f:(unit -> Eventually<'a>) -> Eventually<'a> - - member inline Return: v:'f -> Eventually<'f> - - member inline ReturnFrom: v:'e -> 'e - - member inline TryFinally: e:Eventually<'b> * compensation:(unit -> unit) -> Eventually<'b> - - member inline TryWith: e:Eventually<'c> * handler:(System.Exception -> Eventually<'c>) -> Eventually<'c> - - member inline Zero: unit -> Eventually - -[] -module internal EventuallyAutoOpens = - - val eventually: EventuallyBuilder - /// Generates unique stamps type internal UniqueStampGenerator<'T when 'T: equality> = diff --git a/src/fsharp/fsc.fs b/src/fsharp/fsc.fs index f3a4168efd..e1779881d5 100644 --- a/src/fsharp/fsc.fs +++ b/src/fsharp/fsc.fs @@ -56,6 +56,7 @@ open FSharp.Compiler.Text open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.XmlDocFileWriter +open FSharp.Compiler.BuildGraph //---------------------------------------------------------------------------- // Reporting - warnings, errors @@ -195,7 +196,7 @@ let TypeCheck (ctok, tcConfig, tcImports, tcGlobals, errorLogger: ErrorLogger, a /// copied to the output folder, for example (except perhaps FSharp.Core.dll). /// /// NOTE: there is similar code in IncrementalBuilder.fs and this code should really be reconciled with that -let AdjustForScriptCompile(ctok, tcConfigB: TcConfigBuilder, commandLineSourceFiles, lexResourceManager, dependencyProvider) = +let AdjustForScriptCompile(tcConfigB: TcConfigBuilder, commandLineSourceFiles, lexResourceManager, dependencyProvider) = let combineFilePath file = try @@ -221,7 +222,7 @@ let AdjustForScriptCompile(ctok, tcConfigB: TcConfigBuilder, commandLineSourceFi if IsScript filename then let closure = LoadClosure.ComputeClosureOfScriptFiles - (ctok, tcConfig, [filename, rangeStartup], CodeContext.Compilation, + (tcConfig, [filename, rangeStartup], CodeContext.Compilation, lexResourceManager, dependencyProvider) // Record the new references (non-framework) references from the analysis of the script. (The full resolutions are recorded @@ -466,7 +467,7 @@ let main1(ctok, argv, legacyReferenceResolver, bannerAlreadyPrinted, // Rather than start processing, just collect names, then process them. try let files = ProcessCommandLineFlags (tcConfigB, lcidFromCodePage, argv) - AdjustForScriptCompile(ctok, tcConfigB, files, lexResourceManager, dependencyProvider) + AdjustForScriptCompile(tcConfigB, files, lexResourceManager, dependencyProvider) with e -> errorRecovery e rangeStartup delayForFlagsLogger.ForwardDelayedDiagnostics tcConfigB @@ -516,10 +517,12 @@ let main1(ctok, argv, legacyReferenceResolver, bannerAlreadyPrinted, ReportTime tcConfig "Import mscorlib and FSharp.Core.dll" let foundationalTcConfigP = TcConfigProvider.Constant tcConfig - let sysRes, otherRes, knownUnresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(ctok, tcConfig) + let sysRes, otherRes, knownUnresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(tcConfig) // Import basic assemblies - let tcGlobals, frameworkTcImports = TcImports.BuildFrameworkTcImports (ctok, foundationalTcConfigP, sysRes, otherRes) |> Cancellable.runWithoutCancellation + let tcGlobals, frameworkTcImports = + TcImports.BuildFrameworkTcImports (foundationalTcConfigP, sysRes, otherRes) + |> NodeCode.RunImmediateWithoutCancellation // Register framework tcImports to be disposed in future disposables.Register frameworkTcImports @@ -559,8 +562,8 @@ let main1(ctok, argv, legacyReferenceResolver, bannerAlreadyPrinted, ReportTime tcConfig "Import non-system references" let tcImports = - TcImports.BuildNonFrameworkTcImports(ctok, tcConfigP, frameworkTcImports, otherRes, knownUnresolved, dependencyProvider) - |> Cancellable.runWithoutCancellation + TcImports.BuildNonFrameworkTcImports(tcConfigP, frameworkTcImports, otherRes, knownUnresolved, dependencyProvider) + |> NodeCode.RunImmediateWithoutCancellation // register tcImports to be disposed in future disposables.Register tcImports @@ -667,10 +670,12 @@ let main1OfAst // Resolve assemblies ReportTime tcConfig "Import mscorlib and FSharp.Core.dll" let foundationalTcConfigP = TcConfigProvider.Constant tcConfig - let sysRes, otherRes, knownUnresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(ctok, tcConfig) + let sysRes, otherRes, knownUnresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(tcConfig) // Import basic assemblies - let tcGlobals, frameworkTcImports = TcImports.BuildFrameworkTcImports (ctok, foundationalTcConfigP, sysRes, otherRes) |> Cancellable.runWithoutCancellation + let tcGlobals, frameworkTcImports = + TcImports.BuildFrameworkTcImports (foundationalTcConfigP, sysRes, otherRes) + |> NodeCode.RunImmediateWithoutCancellation // Register framework tcImports to be disposed in future disposables.Register frameworkTcImports @@ -683,7 +688,10 @@ let main1OfAst // Import other assemblies ReportTime tcConfig "Import non-system references" - let tcImports = TcImports.BuildNonFrameworkTcImports(ctok, tcConfigP, frameworkTcImports, otherRes, knownUnresolved, dependencyProvider) |> Cancellable.runWithoutCancellation + + let tcImports = + TcImports.BuildNonFrameworkTcImports(tcConfigP, frameworkTcImports, otherRes, knownUnresolved, dependencyProvider) + |> NodeCode.RunImmediateWithoutCancellation // register tcImports to be disposed in future disposables.Register tcImports diff --git a/src/fsharp/fsi/fsi.fs b/src/fsharp/fsi/fsi.fs index bb567989e1..5596c23169 100644 --- a/src/fsharp/fsi/fsi.fs +++ b/src/fsharp/fsi/fsi.fs @@ -67,6 +67,7 @@ open FSharp.Compiler.Xml open FSharp.Compiler.Tokenization open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps +open FSharp.Compiler.BuildGraph //---------------------------------------------------------------------------- // For the FSI as a service methods... @@ -1571,7 +1572,7 @@ type internal FsiDynamicCompiler let tcConfig = TcConfig.Create(tcConfigB,validate=false) let closure = - LoadClosure.ComputeClosureOfScriptFiles(ctok, tcConfig, + LoadClosure.ComputeClosureOfScriptFiles(tcConfig, sourceFiles, CodeContext.CompilationAndEvaluation, lexResourceManager, fsiOptions.DependencyProvider) @@ -1898,7 +1899,7 @@ module internal MagicAssemblyResolution = // OK, try to resolve as an existing DLL in the resolved reference set. This does unification by assembly name // once an assembly has been referenced. - let searchResult = tcImports.TryFindExistingFullyQualifiedPathBySimpleAssemblyName (ctok, simpleAssemName) + let searchResult = tcImports.TryFindExistingFullyQualifiedPathBySimpleAssemblyName (simpleAssemName) match searchResult with | Some r -> OkResult ([], Choice1Of2 r) @@ -1940,7 +1941,7 @@ module internal MagicAssemblyResolution = #endif // As a last resort, try to find the reference without an extension - match tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef(ctok, ILAssemblyRef.Create(simpleAssemName,None,None,false,None,None)) with + match tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef(ILAssemblyRef.Create(simpleAssemName,None,None,false,None,None)) with | Some(resolvedPath) -> OkResult([],Choice1Of2 resolvedPath) | None -> @@ -2670,11 +2671,11 @@ type internal FsiInteractionProcessor let names = names |> List.filter (fun name -> name.StartsWithOrdinal(stem)) names - member _.ParseAndCheckInteraction (ctok, legacyReferenceResolver, istate, text:string) = + member _.ParseAndCheckInteraction (legacyReferenceResolver, istate, text:string) = let tcConfig = TcConfig.Create(tcConfigB,validate=false) let fsiInteractiveChecker = FsiInteractiveChecker(legacyReferenceResolver, tcConfig, istate.tcGlobals, istate.tcImports, istate.tcState) - fsiInteractiveChecker.ParseAndCheckInteraction(ctok, SourceText.ofString text) + fsiInteractiveChecker.ParseAndCheckInteraction(SourceText.ofString text) //---------------------------------------------------------------------------- @@ -2820,7 +2821,7 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i match fsiOptions.WriteReferencesAndExit with | Some outFile -> let tcConfig = tcConfigP.Get(ctokStartup) - let references, _unresolvedReferences = TcAssemblyResolutions.GetAssemblyResolutionInformation(ctokStartup, tcConfig) + let references, _unresolvedReferences = TcAssemblyResolutions.GetAssemblyResolutionInformation(tcConfig) let lines = [ for r in references -> r.resolvedPath ] FileSystem.OpenFileForWriteShim(outFile).WriteAllLines(lines) exit 0 @@ -2861,13 +2862,15 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i let (tcGlobals,frameworkTcImports,nonFrameworkResolutions,unresolvedReferences) = try let tcConfig = tcConfigP.Get(ctokStartup) - checker.FrameworkImportsCache.Get (ctokStartup, tcConfig) |> Cancellable.runWithoutCancellation + checker.FrameworkImportsCache.Get (tcConfig) + |> NodeCode.RunImmediateWithoutCancellation with e -> stopProcessingRecovery e range0; failwithf "Error creating evaluation session: %A" e let tcImports = try - TcImports.BuildNonFrameworkTcImports(ctokStartup, tcConfigP, frameworkTcImports, nonFrameworkResolutions, unresolvedReferences, fsiOptions.DependencyProvider) |> Cancellable.runWithoutCancellation + TcImports.BuildNonFrameworkTcImports(tcConfigP, frameworkTcImports, nonFrameworkResolutions, unresolvedReferences, fsiOptions.DependencyProvider) + |> NodeCode.RunImmediateWithoutCancellation with e -> stopProcessingRecovery e range0; failwithf "Error creating evaluation session: %A" e @@ -2888,7 +2891,7 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i | Some assembly -> Some (Choice2Of2 assembly) | None -> #endif - match tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef (ctok, aref) with + match tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef (aref) with | Some resolvedPath -> Some (Choice1Of2 resolvedPath) | None -> None @@ -2946,8 +2949,7 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i fsiInteractionProcessor.CompletionsForPartialLID (fsiInteractionProcessor.CurrentState, longIdent) |> Seq.ofList member x.ParseAndCheckInteraction(code) = - let ctok = AssumeCompilationThreadWithoutEvidence () - fsiInteractionProcessor.ParseAndCheckInteraction (ctok, legacyReferenceResolver, fsiInteractionProcessor.CurrentState, code) + fsiInteractionProcessor.ParseAndCheckInteraction (legacyReferenceResolver, fsiInteractionProcessor.CurrentState, code) |> Cancellable.runWithoutCancellation member x.InteractiveChecker = checker diff --git a/src/fsharp/lib.fs b/src/fsharp/lib.fs index 86c105012b..07ba0f0f91 100755 --- a/src/fsharp/lib.fs +++ b/src/fsharp/lib.fs @@ -5,7 +5,9 @@ module internal Internal.Utilities.Library.Extras open System open System.IO open System.Collections.Generic +open System.Threading open System.Threading.Tasks +open System.Globalization open System.Runtime.InteropServices open Internal.Utilities open Internal.Utilities.Collections diff --git a/src/fsharp/lib.fsi b/src/fsharp/lib.fsi index eb73392b51..f7816c129b 100644 --- a/src/fsharp/lib.fsi +++ b/src/fsharp/lib.fsi @@ -2,9 +2,10 @@ module internal Internal.Utilities.Library.Extras -open System.Collections.Generic open System.IO open System.Text +open System.Globalization +open System.Collections.Generic open Internal.Utilities.Collections val debug: bool @@ -289,4 +290,4 @@ module ArrayParallel = val inline map : ('T -> 'U) -> 'T [] -> 'U [] - val inline mapi : (int -> 'T -> 'U) -> 'T [] -> 'U [] \ No newline at end of file + val inline mapi : (int -> 'T -> 'U) -> 'T [] -> 'U [] diff --git a/src/fsharp/service/FSharpCheckerResults.fs b/src/fsharp/service/FSharpCheckerResults.fs index e602a7d94f..dafefae93d 100644 --- a/src/fsharp/service/FSharpCheckerResults.fs +++ b/src/fsharp/service/FSharpCheckerResults.fs @@ -51,6 +51,7 @@ open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.AbstractIL open System.Reflection.PortableExecutable +open FSharp.Compiler.BuildGraph open Internal.Utilities open Internal.Utilities.Collections @@ -1871,8 +1872,7 @@ module internal ParseAndCheckFile = use _unwind = new CompilationGlobalsScope (errHandler.ErrorLogger, BuildPhase.TypeCheck) let! result = - TypeCheckOneInputAndFinishEventually(checkForErrors, tcConfig, tcImports, tcGlobals, None, TcResultsSink.WithSink sink, tcState, parsedMainInput) - |> Eventually.toCancellable + TypeCheckOneInputAndFinish(checkForErrors, tcConfig, tcImports, tcGlobals, None, TcResultsSink.WithSink sink, tcState, parsedMainInput) return result with e -> @@ -2296,7 +2296,7 @@ type FsiInteractiveChecker(legacyReferenceResolver, let keepAssemblyContents = false - member _.ParseAndCheckInteraction (ctok, sourceText: ISourceText, ?userOpName: string) = + member _.ParseAndCheckInteraction (sourceText: ISourceText, ?userOpName: string) = cancellable { let userOpName = defaultArg userOpName "Unknown" let filename = Path.Combine(tcConfig.implicitIncludeDir, "stdin.fsx") @@ -2316,7 +2316,7 @@ type FsiInteractiveChecker(legacyReferenceResolver, CompilerOptions.ParseCompilerOptions (ignore, fsiCompilerOptions, [ ]) let loadClosure = - LoadClosure.ComputeClosureOfScriptText(ctok, legacyReferenceResolver, defaultFSharpBinariesDir, + LoadClosure.ComputeClosureOfScriptText(legacyReferenceResolver, defaultFSharpBinariesDir, filename, sourceText, CodeContext.Editing, tcConfig.useSimpleResolution, tcConfig.useFsiAuxLib, tcConfig.useSdkRefs, tcConfig.sdkDirOverride, new Lexhelp.LexResourceManager(), diff --git a/src/fsharp/service/FSharpCheckerResults.fsi b/src/fsharp/service/FSharpCheckerResults.fsi index 4ad0867d00..a179dd9d8b 100644 --- a/src/fsharp/service/FSharpCheckerResults.fsi +++ b/src/fsharp/service/FSharpCheckerResults.fsi @@ -486,7 +486,6 @@ type internal FsiInteractiveChecker = -> FsiInteractiveChecker member internal ParseAndCheckInteraction : - ctok: CompilationThreadToken * sourceText:ISourceText * ?userOpName: string -> Cancellable diff --git a/src/fsharp/service/IncrementalBuild.fs b/src/fsharp/service/IncrementalBuild.fs index 9600eee88e..093322b4f2 100644 --- a/src/fsharp/service/IncrementalBuild.fs +++ b/src/fsharp/service/IncrementalBuild.fs @@ -39,6 +39,7 @@ open FSharp.Compiler.Text.Range open FSharp.Compiler.Xml open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeOps +open FSharp.Compiler.BuildGraph open Internal.Utilities open Internal.Utilities.Collections @@ -152,7 +153,7 @@ module IncrementalBuildSyntaxTree = | _ -> parse sigNameOpt member _.Invalidate() = - weakCache <- None + SyntaxTree(tcConfig, fileParsed, lexResourceManager, sourceRange, filename, isLastCompiland) member _.FileName = filename @@ -207,16 +208,37 @@ type TcInfoExtras = member x.TcSymbolUses = List.rev x.tcSymbolUsesRev +[] +module TcInfoHelpers = + + let emptyTcInfoExtras = + { + tcResolutionsRev = [] + tcSymbolUsesRev = [] + tcOpenDeclarationsRev = [] + latestImplFile = None + itemKeyStore = None + semanticClassificationKeyStore = None + } + /// Accumulated results of type checking. [] type TcInfoState = | PartialState of TcInfo | FullState of TcInfo * TcInfoExtras - member this.TcInfo = +[] +type TcInfoNode = + | TcInfoNode of partial: GraphNode * full: GraphNode + + member this.HasFull = match this with - | PartialState tcInfo -> tcInfo - | FullState(tcInfo, _) -> tcInfo + | TcInfoNode(_, full) -> full.HasValue + + static member FromState(state: TcInfoState) = + match state with + | PartialState tcInfo -> TcInfoNode(GraphNode(node { return tcInfo }), GraphNode(node { return tcInfo, emptyTcInfoExtras })) + | FullState(tcInfo, tcInfoExtras) -> TcInfoNode(GraphNode(node { return tcInfo }), GraphNode(node { return tcInfo, tcInfoExtras })) /// Bound model of an underlying syntax and typed tree. [] @@ -230,20 +252,46 @@ type BoundModel private (tcConfig: TcConfig, beforeFileChecked: Event, fileChecked: Event, prevTcInfo: TcInfo, - prevTcInfoExtras: (unit -> Eventually), + prevTcInfoExtras: NodeCode, syntaxTreeOpt: SyntaxTree option, - tcInfoStateOpt: TcInfoState option) = + tcInfoStateOpt: TcInfoState option) as this = - let mutable lazyTcInfoState = tcInfoStateOpt - let gate = obj() + let tcInfoNode = + match tcInfoStateOpt with + | Some tcInfoState -> TcInfoNode.FromState(tcInfoState) + | _ -> + let fullGraphNode = + GraphNode(node { + match! this.TypeCheck(false) with + | FullState(tcInfo, tcInfoExtras) -> return tcInfo, tcInfoExtras + | PartialState(tcInfo) -> return tcInfo, emptyTcInfoExtras + }) + + let partialGraphNode = + GraphNode(node { + if enablePartialTypeChecking then + // Optimization so we have less of a chance to duplicate work. + if fullGraphNode.IsComputing then + let! tcInfo, _ = fullGraphNode.GetOrComputeValue() + return tcInfo + else + match fullGraphNode.TryPeekValue() with + | ValueSome(tcInfo, _) -> return tcInfo + | _ -> + match! this.TypeCheck(true) with + | FullState(tcInfo, _) -> return tcInfo + | PartialState(tcInfo) -> return tcInfo + else + let! tcInfo, _ = fullGraphNode.GetOrComputeValue() + return tcInfo + }) + + TcInfoNode(partialGraphNode, fullGraphNode) let defaultTypeCheck () = - eventually { - match prevTcInfoExtras() with - | Eventually.Done(Some prevTcInfoExtras) -> - return FullState(prevTcInfo, prevTcInfoExtras) - | _ -> - return PartialState prevTcInfo + node { + let! prevTcInfoExtras = prevTcInfoExtras + return FullState(prevTcInfo, prevTcInfoExtras) } member _.TcConfig = tcConfig @@ -264,82 +312,77 @@ type BoundModel private (tcConfig: TcConfig, | _ -> None - member this.Invalidate() = - lock gate (fun () -> - let hasSig = this.BackingSignature.IsSome - match lazyTcInfoState with - // If partial checking is enabled and we have a backing sig file, then do nothing. The partial state contains the sig state. - | Some(PartialState _) when enablePartialTypeChecking && hasSig -> () - // If partial checking is enabled and we have a backing sig file, then use the partial state. The partial state contains the sig state. - | Some(FullState(tcInfo, _)) when enablePartialTypeChecking && hasSig -> lazyTcInfoState <- Some(PartialState tcInfo) - | _ -> - lazyTcInfoState <- None + /// If partial type-checking is enabled, + /// this will create a new bound-model that will only have the partial state if the + /// the current bound-model has the full state. + member this.ClearTcInfoExtras() = + let hasSig = this.BackingSignature.IsSome + // If partial checking is enabled and we have a backing sig file, then use the partial state. The partial state contains the sig state. + if tcInfoNode.HasFull && enablePartialTypeChecking && hasSig then // Always invalidate the syntax tree cache. - syntaxTreeOpt - |> Option.iter (fun x -> x.Invalidate()) - ) - - member this.GetState(partialCheck: bool) = - eventually { - let partialCheck = - // Only partial check if we have enabled it. - if enablePartialTypeChecking then partialCheck - else false - - let mustCheck = - match lazyTcInfoState, partialCheck with - | None, _ -> true - | Some(PartialState _), false -> true - | _ -> false - - match lazyTcInfoState with - | Some tcInfoState when not mustCheck -> return tcInfoState - | _ -> - lazyTcInfoState <- None - let! tcInfoState = this.TypeCheck(partialCheck) - lazyTcInfoState <- Some tcInfoState - return tcInfoState - } + let newSyntaxTreeOpt = + syntaxTreeOpt + |> Option.map (fun x -> x.Invalidate()) - member this.TryOptionalExtras() = - eventually { - let! prevState = this.GetState(false) - match prevState with - | FullState(_, prevTcInfoExtras) -> return Some prevTcInfoExtras - | _ -> return None - } + let newTcInfoStateOpt = + match tcInfoNode with + | TcInfoNode(_, fullGraphNode) -> + let tcInfo, _ = fullGraphNode.TryPeekValue().Value + Some(PartialState tcInfo) - member this.Next(syntaxTree) = - eventually { - let! prevState = this.GetState(true) - return - BoundModel( - tcConfig, - tcGlobals, - tcImports, - keepAssemblyContents, - keepAllBackgroundResolutions, - keepAllBackgroundSymbolUses, - enableBackgroundItemKeyStoreAndSemanticClassification, - enablePartialTypeChecking, - beforeFileChecked, - fileChecked, - prevState.TcInfo, - (fun () -> this.TryOptionalExtras()), - Some syntaxTree, - None) - } + BoundModel( + tcConfig, + tcGlobals, + tcImports, + keepAssemblyContents, keepAllBackgroundResolutions, + keepAllBackgroundSymbolUses, + enableBackgroundItemKeyStoreAndSemanticClassification, + enablePartialTypeChecking, + beforeFileChecked, + fileChecked, + prevTcInfo, + prevTcInfoExtras, + newSyntaxTreeOpt, + newTcInfoStateOpt) + else + this + + member this.Next(syntaxTree, tcInfo) = + BoundModel( + tcConfig, + tcGlobals, + tcImports, + keepAssemblyContents, + keepAllBackgroundResolutions, + keepAllBackgroundSymbolUses, + enableBackgroundItemKeyStoreAndSemanticClassification, + enablePartialTypeChecking, + beforeFileChecked, + fileChecked, + tcInfo, + this.GetTcInfoExtras(), + Some syntaxTree, + None) member this.Finish(finalTcErrorsRev, finalTopAttribs) = - eventually { - let! state = this.GetState(true) - - let finishTcInfo = { state.TcInfo with tcErrorsRev = finalTcErrorsRev; topAttribs = finalTopAttribs } - let finishState = - match state with - | PartialState(_) -> PartialState(finishTcInfo) - | FullState(_, tcInfoExtras) -> FullState(finishTcInfo, tcInfoExtras) + node { + let createFinish tcInfo = + { tcInfo with tcErrorsRev = finalTcErrorsRev; topAttribs = finalTopAttribs } + + let! finishState = + node { + match tcInfoNode with + | TcInfoNode(partialGraphNode, fullGraphNode) -> + if fullGraphNode.HasValue then + let! tcInfo, tcInfoExtras = fullGraphNode.GetOrComputeValue() + let finishTcInfo = createFinish tcInfo + return FullState(finishTcInfo, tcInfoExtras) + else + let! tcInfo = partialGraphNode.GetOrComputeValue() + let finishTcInfo = createFinish tcInfo + return PartialState(finishTcInfo) + } return BoundModel( @@ -360,47 +403,45 @@ type BoundModel private (tcConfig: TcConfig, } member this.GetTcInfo() = - eventually { - let! state = this.GetState(true) - return state.TcInfo - } + match tcInfoNode with + | TcInfoNode(partialGraphNode, _) -> + partialGraphNode.GetOrComputeValue() member this.TryTcInfo = - match lazyTcInfoState with - | Some(state) -> - match state with - | FullState(tcInfo, _) - | PartialState(tcInfo) -> Some tcInfo - | _ -> None + match tcInfoNode with + | TcInfoNode(partialGraphNode, fullGraphNode) -> + match partialGraphNode.TryPeekValue() with + | ValueSome tcInfo -> Some tcInfo + | _ -> + match fullGraphNode.TryPeekValue() with + | ValueSome(tcInfo, _) -> Some tcInfo + | _ -> None + + member this.GetTcInfoExtras() : NodeCode = + match tcInfoNode with + | TcInfoNode(_, fullGraphNode) -> + node { + let! _, tcInfoExtras = fullGraphNode.GetOrComputeValue() + return tcInfoExtras + } member this.GetTcInfoWithExtras() = - eventually { - let! state = this.GetState(false) - match state with - | FullState(tcInfo, tcInfoExtras) -> return tcInfo, tcInfoExtras - | PartialState tcInfo -> - return - tcInfo, - { - tcResolutionsRev = [] - tcSymbolUsesRev = [] - tcOpenDeclarationsRev = [] - latestImplFile = None - itemKeyStore = None - semanticClassificationKeyStore = None - } - } + match tcInfoNode with + | TcInfoNode(_, fullGraphNode) -> + fullGraphNode.GetOrComputeValue() - member private this.TypeCheck (partialCheck: bool) = - match partialCheck, lazyTcInfoState with + member private this.TypeCheck (partialCheck: bool) : NodeCode = + match partialCheck, tcInfoStateOpt with | true, Some (PartialState _ as state) - | true, Some (FullState _ as state) -> state |> Eventually.Done - | false, Some (FullState _ as state) -> state |> Eventually.Done + | true, Some (FullState _ as state) -> node { return state } + | false, Some (FullState _ as state) -> node { return state } | _ -> - eventually { + node { match syntaxTreeOpt with - | None -> return! defaultTypeCheck () + | None -> + let! res = defaultTypeCheck () + return res | Some syntaxTree -> let sigNameOpt = if partialCheck then @@ -413,101 +454,98 @@ type BoundModel private (tcConfig: TcConfig, IncrementalBuilderEventTesting.MRU.Add(IncrementalBuilderEventTesting.IBETypechecked filename) let capturingErrorLogger = CompilationErrorLogger("TypeCheck", tcConfig.errorSeverityOptions) let errorLogger = GetErrorLoggerFilteringByScopedPragmas(false, GetScopedPragmasForInput input, capturingErrorLogger) - - // This reinstalls the CompilationGlobalsScope each time the Eventually is restarted, potentially - // on a new thread. This is needed because CompilationGlobalsScope installs thread local variables. - return! Eventually.reusing (fun () -> new CompilationGlobalsScope (errorLogger, BuildPhase.TypeCheck) :> IDisposable) <| eventually { - - beforeFileChecked.Trigger filename - let prevModuleNamesDict = prevTcInfo.moduleNamesDict - let prevTcState = prevTcInfo.tcState - let prevTcErrorsRev = prevTcInfo.tcErrorsRev - let prevTcDependencyFiles = prevTcInfo.tcDependencyFiles - - ApplyMetaCommandsFromInputToTcConfig (tcConfig, input, Path.GetDirectoryName filename, tcImports.DependencyProvider) |> ignore - let sink = TcResultsSinkImpl(tcGlobals) - let hadParseErrors = not (Array.isEmpty parseErrors) - let input, moduleNamesDict = DeduplicateParsedInputModuleName prevModuleNamesDict input - - Logger.LogBlockMessageStart filename LogCompilerFunctionId.IncrementalBuild_TypeCheck - let! (tcEnvAtEndOfFile, topAttribs, implFile, ccuSigForFile), tcState = - TypeCheckOneInputEventually - ((fun () -> hadParseErrors || errorLogger.ErrorCount > 0), - tcConfig, tcImports, - tcGlobals, - None, - (if partialCheck then TcResultsSink.NoSink else TcResultsSink.WithSink sink), - prevTcState, input, - partialCheck) - Logger.LogBlockMessageStop filename LogCompilerFunctionId.IncrementalBuild_TypeCheck - - fileChecked.Trigger filename - let newErrors = Array.append parseErrors (capturingErrorLogger.GetDiagnostics()) - - let tcEnvAtEndOfFile = if keepAllBackgroundResolutions then tcEnvAtEndOfFile else tcState.TcEnvFromImpls - - let tcInfo = + use _ = new CompilationGlobalsScope(errorLogger, BuildPhase.TypeCheck) + + beforeFileChecked.Trigger filename + let prevModuleNamesDict = prevTcInfo.moduleNamesDict + let prevTcState = prevTcInfo.tcState + let prevTcErrorsRev = prevTcInfo.tcErrorsRev + let prevTcDependencyFiles = prevTcInfo.tcDependencyFiles + + ApplyMetaCommandsFromInputToTcConfig (tcConfig, input, Path.GetDirectoryName filename, tcImports.DependencyProvider) |> ignore + let sink = TcResultsSinkImpl(tcGlobals) + let hadParseErrors = not (Array.isEmpty parseErrors) + let input, moduleNamesDict = DeduplicateParsedInputModuleName prevModuleNamesDict input + + Logger.LogBlockMessageStart filename LogCompilerFunctionId.IncrementalBuild_TypeCheck + + let! (tcEnvAtEndOfFile, topAttribs, implFile, ccuSigForFile), tcState = + TypeCheckOneInput + ((fun () -> hadParseErrors || errorLogger.ErrorCount > 0), + tcConfig, tcImports, + tcGlobals, + None, + (if partialCheck then TcResultsSink.NoSink else TcResultsSink.WithSink sink), + prevTcState, input, + partialCheck) + |> NodeCode.FromCancellable + + Logger.LogBlockMessageStop filename LogCompilerFunctionId.IncrementalBuild_TypeCheck + + fileChecked.Trigger filename + let newErrors = Array.append parseErrors (capturingErrorLogger.GetDiagnostics()) + + let tcEnvAtEndOfFile = if keepAllBackgroundResolutions then tcEnvAtEndOfFile else tcState.TcEnvFromImpls + + let tcInfo = + { + tcState = tcState + tcEnvAtEndOfFile = tcEnvAtEndOfFile + moduleNamesDict = moduleNamesDict + latestCcuSigForFile = Some ccuSigForFile + tcErrorsRev = newErrors :: prevTcErrorsRev + topAttribs = Some topAttribs + tcDependencyFiles = filename :: prevTcDependencyFiles + sigNameOpt = + match input with + | ParsedInput.SigFile(ParsedSigFileInput(fileName=fileName;qualifiedNameOfFile=qualName)) -> + Some(fileName, qualName) + | _ -> + None + } + + if partialCheck then + return PartialState tcInfo + else + let! prevTcInfoOptional = prevTcInfoExtras + // Build symbol keys + let itemKeyStore, semanticClassification = + if enableBackgroundItemKeyStoreAndSemanticClassification then + Logger.LogBlockMessageStart filename LogCompilerFunctionId.IncrementalBuild_CreateItemKeyStoreAndSemanticClassification + let sResolutions = sink.GetResolutions() + let builder = ItemKeyStoreBuilder() + let preventDuplicates = HashSet({ new IEqualityComparer with + member _.Equals((s1, e1): struct(pos * pos), (s2, e2): struct(pos * pos)) = Position.posEq s1 s2 && Position.posEq e1 e2 + member _.GetHashCode o = o.GetHashCode() }) + sResolutions.CapturedNameResolutions + |> Seq.iter (fun cnr -> + let r = cnr.Range + if preventDuplicates.Add struct(r.Start, r.End) then + builder.Write(cnr.Range, cnr.Item)) + + let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None) + + let sckBuilder = SemanticClassificationKeyStoreBuilder() + sckBuilder.WriteAll semanticClassification + + let res = builder.TryBuildAndReset(), sckBuilder.TryBuildAndReset() + Logger.LogBlockMessageStop filename LogCompilerFunctionId.IncrementalBuild_CreateItemKeyStoreAndSemanticClassification + res + else + None, None + + let tcInfoExtras = { - tcState = tcState - tcEnvAtEndOfFile = tcEnvAtEndOfFile - moduleNamesDict = moduleNamesDict - latestCcuSigForFile = Some ccuSigForFile - tcErrorsRev = newErrors :: prevTcErrorsRev - topAttribs = Some topAttribs - tcDependencyFiles = filename :: prevTcDependencyFiles - sigNameOpt = - match input with - | ParsedInput.SigFile(ParsedSigFileInput(fileName=fileName;qualifiedNameOfFile=qualName)) -> - Some(fileName, qualName) - | _ -> - None + /// Only keep the typed interface files when doing a "full" build for fsc.exe, otherwise just throw them away + latestImplFile = if keepAssemblyContents then implFile else None + tcResolutionsRev = (if keepAllBackgroundResolutions then sink.GetResolutions() else TcResolutions.Empty) :: prevTcInfoOptional.tcResolutionsRev + tcSymbolUsesRev = (if keepAllBackgroundSymbolUses then sink.GetSymbolUses() else TcSymbolUses.Empty) :: prevTcInfoOptional.tcSymbolUsesRev + tcOpenDeclarationsRev = sink.GetOpenDeclarations() :: prevTcInfoOptional.tcOpenDeclarationsRev + itemKeyStore = itemKeyStore + semanticClassificationKeyStore = semanticClassification } - - if partialCheck then - return PartialState tcInfo - else - match! prevTcInfoExtras() with - | None -> return PartialState tcInfo - | Some prevTcInfoOptional -> - // Build symbol keys - let itemKeyStore, semanticClassification = - if enableBackgroundItemKeyStoreAndSemanticClassification then - Logger.LogBlockMessageStart filename LogCompilerFunctionId.IncrementalBuild_CreateItemKeyStoreAndSemanticClassification - let sResolutions = sink.GetResolutions() - let builder = ItemKeyStoreBuilder() - let preventDuplicates = HashSet({ new IEqualityComparer with - member _.Equals((s1, e1): struct(pos * pos), (s2, e2): struct(pos * pos)) = Position.posEq s1 s2 && Position.posEq e1 e2 - member _.GetHashCode o = o.GetHashCode() }) - sResolutions.CapturedNameResolutions - |> Seq.iter (fun cnr -> - let r = cnr.Range - if preventDuplicates.Add struct(r.Start, r.End) then - builder.Write(cnr.Range, cnr.Item)) - - let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None) - - let sckBuilder = SemanticClassificationKeyStoreBuilder() - sckBuilder.WriteAll semanticClassification - - let res = builder.TryBuildAndReset(), sckBuilder.TryBuildAndReset() - Logger.LogBlockMessageStop filename LogCompilerFunctionId.IncrementalBuild_CreateItemKeyStoreAndSemanticClassification - res - else - None, None - - let tcInfoExtras = - { - /// Only keep the typed interface files when doing a "full" build for fsc.exe, otherwise just throw them away - latestImplFile = if keepAssemblyContents then implFile else None - tcResolutionsRev = (if keepAllBackgroundResolutions then sink.GetResolutions() else TcResolutions.Empty) :: prevTcInfoOptional.tcResolutionsRev - tcSymbolUsesRev = (if keepAllBackgroundSymbolUses then sink.GetSymbolUses() else TcSymbolUses.Empty) :: prevTcInfoOptional.tcSymbolUsesRev - tcOpenDeclarationsRev = sink.GetOpenDeclarations() :: prevTcInfoOptional.tcOpenDeclarationsRev - itemKeyStore = itemKeyStore - semanticClassificationKeyStore = semanticClassification - } - - return FullState(tcInfo, tcInfoExtras) - } + + return FullState(tcInfo, tcInfoExtras) } static member Create(tcConfig: TcConfig, @@ -520,7 +558,7 @@ type BoundModel private (tcConfig: TcConfig, beforeFileChecked: Event, fileChecked: Event, prevTcInfo: TcInfo, - prevTcInfoExtras: (unit -> Eventually), + prevTcInfoExtras: NodeCode, syntaxTreeOpt: SyntaxTree option) = BoundModel(tcConfig, tcGlobals, tcImports, keepAssemblyContents, keepAllBackgroundResolutions, @@ -540,46 +578,56 @@ type FrameworkImportsCacheKey = (*resolvedpath*)string list * string * (*TargetF /// Represents a cache of 'framework' references that can be shared between multiple incremental builds type FrameworkImportsCache(size) = + let gate = obj() + // Mutable collection protected via CompilationThreadToken - let frameworkTcImportsCache = AgedLookup(size, areSimilar=(fun (x, y) -> x = y)) + let frameworkTcImportsCache = AgedLookup>(size, areSimilar=(fun (x, y) -> x = y)) /// Reduce the size of the cache in low-memory scenarios - member _.Downsize ctok = frameworkTcImportsCache.Resize(ctok, newKeepStrongly=0) + member _.Downsize() = frameworkTcImportsCache.Resize(AnyCallerThread, newKeepStrongly=0) /// Clear the cache - member _.Clear ctok = frameworkTcImportsCache.Clear ctok + member _.Clear() = frameworkTcImportsCache.Clear AnyCallerThread /// This function strips the "System" assemblies from the tcConfig and returns a age-cached TcImports for them. - member _.Get(ctok, tcConfig: TcConfig) = - cancellable { - // Split into installed and not installed. - let frameworkDLLs, nonFrameworkResolutions, unresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(ctok, tcConfig) + member _.GetNode(tcConfig: TcConfig, frameworkDLLs: AssemblyResolution list, nonFrameworkResolutions: AssemblyResolution list) = let frameworkDLLsKey = frameworkDLLs |> List.map (fun ar->ar.resolvedPath) // The cache key. Just the minimal data. |> List.sort // Sort to promote cache hits. - let! tcGlobals, frameworkTcImports = - cancellable { - // Prepare the frameworkTcImportsCache - // - // The data elements in this key are very important. There should be nothing else in the TcConfig that logically affects - // the import of a set of framework DLLs into F# CCUs. That is, the F# CCUs that result from a set of DLLs (including - // FSharp.Core.dll and mscorlib.dll) must be logically invariant of all the other compiler configuration parameters. - let key = (frameworkDLLsKey, - tcConfig.primaryAssembly.Name, - tcConfig.GetTargetFrameworkDirectories(), - tcConfig.fsharpBinariesDir, - tcConfig.langVersion.SpecifiedVersion) - - match frameworkTcImportsCache.TryGet (ctok, key) with - | Some res -> return res - | None -> - let tcConfigP = TcConfigProvider.Constant tcConfig - let! ((tcGlobals, tcImports) as res) = TcImports.BuildFrameworkTcImports (ctok, tcConfigP, frameworkDLLs, nonFrameworkResolutions) - frameworkTcImportsCache.Put(ctok, key, res) - return tcGlobals, tcImports - } + // Prepare the frameworkTcImportsCache + // + // The data elements in this key are very important. There should be nothing else in the TcConfig that logically affects + // the import of a set of framework DLLs into F# CCUs. That is, the F# CCUs that result from a set of DLLs (including + // FSharp.Core.dll and mscorlib.dll) must be logically invariant of all the other compiler configuration parameters. + let key = (frameworkDLLsKey, + tcConfig.primaryAssembly.Name, + tcConfig.GetTargetFrameworkDirectories(), + tcConfig.fsharpBinariesDir, + tcConfig.langVersion.SpecifiedVersion) + + let node = + lock gate (fun () -> + match frameworkTcImportsCache.TryGet (AnyCallerThread, key) with + | Some lazyWork -> lazyWork + | None -> + let lazyWork = GraphNode(node { + let tcConfigP = TcConfigProvider.Constant tcConfig + return! TcImports.BuildFrameworkTcImports (tcConfigP, frameworkDLLs, nonFrameworkResolutions) + }) + frameworkTcImportsCache.Put(AnyCallerThread, key, lazyWork) + lazyWork + ) + node + + /// This function strips the "System" assemblies from the tcConfig and returns a age-cached TcImports for them. + member this.Get(tcConfig: TcConfig) = + node { + // Split into installed and not installed. + let frameworkDLLs, nonFrameworkResolutions, unresolved = TcAssemblyResolutions.SplitNonFoundationalResolutions(tcConfig) + let node = this.GetNode(tcConfig, frameworkDLLs, nonFrameworkResolutions) + let! tcGlobals, frameworkTcImports = node.GetOrComputeValue() return tcGlobals, frameworkTcImports, nonFrameworkResolutions, unresolved } @@ -602,13 +650,13 @@ type PartialCheckResults (boundModel: BoundModel, timeStamp: DateTime) = member _.GetTcInfoWithExtras() = boundModel.GetTcInfoWithExtras() member _.TryGetItemKeyStore() = - eventually { + node { let! _, info = boundModel.GetTcInfoWithExtras() return info.itemKeyStore } member _.GetSemanticClassification() = - eventually { + node { let! _, info = boundModel.GetTcInfoWithExtras() return info.semanticClassificationKeyStore } @@ -625,9 +673,8 @@ module Utilities = /// Constructs the build data (IRawFSharpAssemblyData) representing the assembly when used /// as a cross-assembly reference. Note the assembly has not been generated on disk, so this is /// a virtualized view of the assembly contents as computed by background checking. -type RawFSharpAssemblyDataBackedByLanguageService (tcConfig, tcGlobals, tcState: TcState, outfile, topAttrs, assemblyName, ilAssemRef) = +type RawFSharpAssemblyDataBackedByLanguageService (tcConfig, tcGlobals, generatedCcu: CcuThunk, outfile, topAttrs, assemblyName, ilAssemRef) = - let generatedCcu = tcState.Ccu let exportRemapping = MakeExportRemapping generatedCcu generatedCcu.Contents let sigData = @@ -660,74 +707,39 @@ type IncrementalBuilderState = stampedFileNames: ImmutableArray logicalStampedFileNames: ImmutableArray stampedReferencedAssemblies: ImmutableArray - initialBoundModel: BoundModel option - boundModels: ImmutableArray - finalizedBoundModel: ((ILAssemblyRef * IRawFSharpAssemblyData option * TypedImplFile list option * BoundModel) * DateTime) option - enablePartialTypeChecking: bool + initialBoundModel: GraphNode + boundModels: ImmutableArray> + finalizedBoundModel: GraphNode<((ILAssemblyRef * IRawFSharpAssemblyData option * TypedImplFile list option * BoundModel) * DateTime)> } /// Manages an incremental build graph for the build of a single F# project -type IncrementalBuilder(tcGlobals, - frameworkTcImports, - nonFrameworkAssemblyInputs, - nonFrameworkResolutions, - unresolvedReferences, - tcConfig: TcConfig, - projectDirectory, - outfile, - assemblyName, - niceNameGen: NiceNameGenerator, - lexResourceManager, - sourceFiles, - loadClosureOpt: LoadClosure option, - keepAssemblyContents, - keepAllBackgroundResolutions, - keepAllBackgroundSymbolUses, - enableBackgroundItemKeyStoreAndSemanticClassification, - enablePartialTypeChecking, - dependencyProviderOpt: DependencyProvider option) = - - let tcConfigP = TcConfigProvider.Constant tcConfig - let fileParsed = new Event() - let beforeFileChecked = new Event() - let fileChecked = new Event() - let projectChecked = new Event() +type IncrementalBuilder( + initialBoundModel: BoundModel, + tcGlobals, + nonFrameworkAssemblyInputs, + tcConfig: TcConfig, + outfile, + assemblyName, + lexResourceManager, + sourceFiles, + enablePartialTypeChecking, + beforeFileChecked: Event, + fileChecked: Event, #if !NO_EXTENSIONTYPING - let importsInvalidatedByTypeProvider = new Event() + importsInvalidatedByTypeProvider: Event, #endif - let defaultPartialTypeChecking = enablePartialTypeChecking + allDependencies) = - // Check for the existence of loaded sources and prepend them to the sources list if present. - let sourceFiles = tcConfig.GetAvailableLoadedSources() @ (sourceFiles |>List.map (fun s -> rangeStartup, s)) - - // Mark up the source files with an indicator flag indicating if they are the last source file in the project - let sourceFiles = - let flags, isExe = tcConfig.ComputeCanContainEntryPoint(sourceFiles |> List.map snd) - ((sourceFiles, flags) ||> List.map2 (fun (m, nm) flag -> (m, nm, (flag, isExe)))) + let fileParsed = new Event() + let projectChecked = new Event() let defaultTimeStamp = DateTime.UtcNow - let basicDependencies = - [ for (UnresolvedAssemblyReference(referenceText, _)) in unresolvedReferences do - // Exclude things that are definitely not a file name - if not(FileSystem.IsInvalidPathShim referenceText) then - let file = if FileSystem.IsPathRootedShim referenceText then referenceText else Path.Combine(projectDirectory, referenceText) - yield file + let mutable isImportsInvalidated = false - for r in nonFrameworkResolutions do - yield r.resolvedPath ] - - let allDependencies = - [| yield! basicDependencies - for (_, f, _) in sourceFiles do - yield f |] - - // For scripts, the dependency provider is already available. - // For projects create a fresh one for the project. - let dependencyProvider = - match dependencyProviderOpt with - | None -> new DependencyProvider() - | Some dependencyProvider -> dependencyProvider +#if !NO_EXTENSIONTYPING + do importsInvalidatedByTypeProvider.Publish.Add(fun () -> isImportsInvalidated <- true) +#endif //---------------------------------------------------- // START OF BUILD TASK FUNCTIONS @@ -736,25 +748,39 @@ type IncrementalBuilder(tcGlobals, let StampFileNameTask (cache: TimeStampCache) (_m: range, filename: string, _isLastCompiland) = cache.GetFileTimeStamp filename - /// Parse the given file and return the given input. - let ParseTask (sourceRange: range, filename: string, isLastCompiland) = - SyntaxTree(tcConfig, fileParsed, lexResourceManager, sourceRange, filename, isLastCompiland) - /// Timestamps of referenced assemblies are taken from the file's timestamp. let StampReferencedAssemblyTask (cache: TimeStampCache) (_ref, timeStamper) = timeStamper cache // Link all the assemblies together and produce the input typecheck accumulator - let CombineImportedAssembliesTask ctok : Cancellable = - cancellable { + static let CombineImportedAssembliesTask ( + assemblyName, + tcConfig: TcConfig, + tcConfigP, + tcGlobals, + frameworkTcImports, + nonFrameworkResolutions, + unresolvedReferences, + dependencyProvider, + loadClosureOpt: LoadClosure option, + niceNameGen, + basicDependencies, + keepAssemblyContents, + keepAllBackgroundResolutions, + keepAllBackgroundSymbolUses, + enableBackgroundItemKeyStoreAndSemanticClassification, + defaultPartialTypeChecking, + beforeFileChecked, + fileChecked, + importsInvalidatedByTypeProvider: Event) : NodeCode = + node { let errorLogger = CompilationErrorLogger("CombineImportedAssembliesTask", tcConfig.errorSeverityOptions) - // Return the disposable object that cleans up - use _holder = new CompilationGlobalsScope(errorLogger, BuildPhase.Parameter) + use _ = new CompilationGlobalsScope(errorLogger, BuildPhase.Parameter) let! tcImports = - cancellable { + node { try - let! tcImports = TcImports.BuildNonFrameworkTcImports(ctok, tcConfigP, frameworkTcImports, nonFrameworkResolutions, unresolvedReferences, dependencyProvider) + let! tcImports = TcImports.BuildNonFrameworkTcImports(tcConfigP, frameworkTcImports, nonFrameworkResolutions, unresolvedReferences, dependencyProvider) #if !NO_EXTENSIONTYPING tcImports.GetCcusExcludingBase() |> Seq.iter (fun ccu -> // When a CCU reports an invalidation, merge them together and just report a @@ -772,9 +798,9 @@ type IncrementalBuilder(tcGlobals, // In the invalidation handler we use a weak reference to allow the IncrementalBuilder to // be collected if, for some reason, a TP instance is not disposed or not GC'd. let capturedImportsInvalidated = WeakReference<_>(importsInvalidatedByTypeProvider) - ccu.Deref.InvalidateEvent.Add(fun msg -> + ccu.Deref.InvalidateEvent.Add(fun _ -> match capturedImportsInvalidated.TryGetTarget() with - | true, tg -> tg.Trigger msg + | true, tg -> tg.Trigger() | _ -> ())) #endif return tcImports @@ -805,15 +831,7 @@ type IncrementalBuilder(tcGlobals, tcDependencyFiles = basicDependencies sigNameOpt = None } - let tcInfoExtras = - { - tcResolutionsRev=[] - tcSymbolUsesRev=[] - tcOpenDeclarationsRev=[] - latestImplFile=None - itemKeyStore = None - semanticClassificationKeyStore = None - } + let tcInfoExtras = emptyTcInfoExtras return BoundModel.Create( tcConfig, @@ -827,43 +845,52 @@ type IncrementalBuilder(tcGlobals, beforeFileChecked, fileChecked, tcInfo, - (fun () -> Eventually.Done (Some tcInfoExtras)), + node { return tcInfoExtras }, None) } - /// Type check all files. - let TypeCheckTask ctok enablePartialTypeChecking (prevBoundModel: BoundModel) syntaxTree: Eventually = - eventually { - RequireCompilationThread ctok - let! boundModel = prevBoundModel.Next(syntaxTree) + /// Type check all files eagerly. + let TypeCheckTask partialCheck (prevBoundModel: BoundModel) syntaxTree: NodeCode = + node { + let! tcInfo = prevBoundModel.GetTcInfo() + let boundModel = prevBoundModel.Next(syntaxTree, tcInfo) + // Eagerly type check // We need to do this to keep the expected behavior of events (namely fileChecked) when checking a file/project. - let! _ = boundModel.GetState(enablePartialTypeChecking) + if partialCheck then + let! _ = boundModel.GetTcInfo() + () + else + let! _ = boundModel.GetTcInfoWithExtras() + () + return boundModel } /// Finish up the typechecking to produce outputs for the rest of the compilation process - let FinalizeTypeCheckTask ctok enablePartialTypeChecking (boundModels: ImmutableArray) = - eventually { - DoesNotRequireCompilerThreadTokenAndCouldPossiblyBeMadeConcurrent ctok - - let errorLogger = CompilationErrorLogger("CombineImportedAssembliesTask", tcConfig.errorSeverityOptions) - // This reinstalls the CompilationGlobalsScope each time the Eventually is restarted, potentially - // on a new thread. This is needed because CompilationGlobalsScope installs thread local variables. - return! Eventually.reusing (fun () -> new CompilationGlobalsScope (errorLogger, BuildPhase.TypeCheck) :> IDisposable) <| eventually { + let FinalizeTypeCheckTask (boundModels: ImmutableArray) = + node { + let errorLogger = CompilationErrorLogger("FinalizeTypeCheckTask", tcConfig.errorSeverityOptions) + use _ = new CompilationGlobalsScope(errorLogger, BuildPhase.TypeCheck) let! results = - boundModels |> Eventually.each (fun boundModel -> eventually { - let! tcInfo, latestImplFile = - eventually { - if enablePartialTypeChecking then - let! tcInfo = boundModel.GetTcInfo() - return tcInfo, None - else - let! tcInfo, tcInfoExtras = boundModel.GetTcInfoWithExtras() - return tcInfo, tcInfoExtras.latestImplFile - } - return (tcInfo.tcEnvAtEndOfFile, defaultArg tcInfo.topAttribs EmptyTopAttrs, latestImplFile, tcInfo.latestCcuSigForFile) + boundModels + |> Seq.map (fun boundModel -> node { + if enablePartialTypeChecking then + let! tcInfo = boundModel.GetTcInfo() + return tcInfo, None + else + let! tcInfo, tcInfoExtras = boundModel.GetTcInfoWithExtras() + return tcInfo, tcInfoExtras.latestImplFile }) + |> Seq.map (fun work -> + node { + let! tcInfo, latestImplFile = work + return (tcInfo.tcEnvAtEndOfFile, defaultArg tcInfo.topAttribs EmptyTopAttrs, latestImplFile, tcInfo.latestCcuSigForFile) + } + ) + |> NodeCode.Sequential + + let results = results |> List.ofSeq // Get the state at the end of the type-checking of the last file let finalBoundModel = boundModels.[boundModels.Length-1] @@ -876,55 +903,49 @@ type IncrementalBuilder(tcGlobals, let ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt = try - // TypeCheckClosedInputSetFinish fills in tcState.Ccu but in incremental scenarios we don't want this, - // so we make this temporary here - let oldContents = tcState.Ccu.Deref.Contents - try - let tcState, tcAssemblyExpr = TypeCheckClosedInputSetFinish (mimpls, tcState) - - // Compute the identity of the generated assembly based on attributes, options etc. - // Some of this is duplicated from fsc.fs - let ilAssemRef = - let publicKey = - try - let signingInfo = ValidateKeySigningAttributes (tcConfig, tcGlobals, topAttrs) - match GetStrongNameSigner signingInfo with - | None -> None - | Some s -> Some (PublicKey.KeyAsToken(s.PublicKey)) - with e -> - errorRecoveryNoRange e - None - let locale = TryFindFSharpStringAttribute tcGlobals (tcGlobals.FindSysAttrib "System.Reflection.AssemblyCultureAttribute") topAttrs.assemblyAttrs - let assemVerFromAttrib = - TryFindFSharpStringAttribute tcGlobals (tcGlobals.FindSysAttrib "System.Reflection.AssemblyVersionAttribute") topAttrs.assemblyAttrs - |> Option.bind (fun v -> try Some (parseILVersion v) with _ -> None) - let ver = - match assemVerFromAttrib with - | None -> tcConfig.version.GetVersionInfo(tcConfig.implicitIncludeDir) - | Some v -> v - ILAssemblyRef.Create(assemblyName, None, publicKey, false, Some ver, locale) - - let tcAssemblyDataOpt = - try + let tcState, tcAssemblyExpr, ccuContents = TypeCheckClosedInputSetFinish (mimpls, tcState) - // Assemblies containing type provider components can not successfully be used via cross-assembly references. - // We return 'None' for the assembly portion of the cross-assembly reference - let hasTypeProviderAssemblyAttrib = - topAttrs.assemblyAttrs |> List.exists (fun (Attrib(tcref, _, _, _, _, _, _)) -> - let nm = tcref.CompiledRepresentationForNamedType.BasicQualifiedName - nm = typeof.FullName) - - if tcState.CreatesGeneratedProvidedTypes || hasTypeProviderAssemblyAttrib then - None - else - Some (RawFSharpAssemblyDataBackedByLanguageService (tcConfig, tcGlobals, tcState, outfile, topAttrs, assemblyName, ilAssemRef) :> IRawFSharpAssemblyData) + let generatedCcu = tcState.Ccu.CloneWithFinalizedContents(ccuContents) + // Compute the identity of the generated assembly based on attributes, options etc. + // Some of this is duplicated from fsc.fs + let ilAssemRef = + let publicKey = + try + let signingInfo = ValidateKeySigningAttributes (tcConfig, tcGlobals, topAttrs) + match GetStrongNameSigner signingInfo with + | None -> None + | Some s -> Some (PublicKey.KeyAsToken(s.PublicKey)) with e -> errorRecoveryNoRange e None - ilAssemRef, tcAssemblyDataOpt, Some tcAssemblyExpr - finally - tcState.Ccu.Deref.Contents <- oldContents + let locale = TryFindFSharpStringAttribute tcGlobals (tcGlobals.FindSysAttrib "System.Reflection.AssemblyCultureAttribute") topAttrs.assemblyAttrs + let assemVerFromAttrib = + TryFindFSharpStringAttribute tcGlobals (tcGlobals.FindSysAttrib "System.Reflection.AssemblyVersionAttribute") topAttrs.assemblyAttrs + |> Option.bind (fun v -> try Some (parseILVersion v) with _ -> None) + let ver = + match assemVerFromAttrib with + | None -> tcConfig.version.GetVersionInfo(tcConfig.implicitIncludeDir) + | Some v -> v + ILAssemblyRef.Create(assemblyName, None, publicKey, false, Some ver, locale) + + let tcAssemblyDataOpt = + try + // Assemblies containing type provider components can not successfully be used via cross-assembly references. + // We return 'None' for the assembly portion of the cross-assembly reference + let hasTypeProviderAssemblyAttrib = + topAttrs.assemblyAttrs |> List.exists (fun (Attrib(tcref, _, _, _, _, _, _)) -> + let nm = tcref.CompiledRepresentationForNamedType.BasicQualifiedName + nm = typeof.FullName) + + if tcState.CreatesGeneratedProvidedTypes || hasTypeProviderAssemblyAttrib then + None + else + Some (RawFSharpAssemblyDataBackedByLanguageService (tcConfig, tcGlobals, generatedCcu, outfile, topAttrs, assemblyName, ilAssemRef) :> IRawFSharpAssemblyData) + with e -> + errorRecoveryNoRange e + None + ilAssemRef, tcAssemblyDataOpt, Some tcAssemblyExpr with e -> errorRecoveryNoRange e mkSimpleAssemblyRef assemblyName, None, None @@ -932,7 +953,6 @@ type IncrementalBuilder(tcGlobals, let diagnostics = errorLogger.GetDiagnostics() :: finalInfo.tcErrorsRev let! finalBoundModelWithErrors = finalBoundModel.Finish(diagnostics, Some topAttrs) return ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt, finalBoundModelWithErrors - } } // END OF BUILD TASK FUNCTIONS @@ -941,20 +961,50 @@ type IncrementalBuilder(tcGlobals, // --------------------------------------------------------------------------------------------- // START OF BUILD DESCRIPTION + let GetSyntaxTree (sourceRange: range, filename: string, isLastCompiland) = + SyntaxTree(tcConfig, fileParsed, lexResourceManager, sourceRange, filename, isLastCompiland) + // Inputs let fileNames = sourceFiles |> Array.ofList // TODO: This should be an immutable array. let referencedAssemblies = nonFrameworkAssemblyInputs |> Array.ofList // TODO: This should be an immutable array. - let computeStampedFileName (state: IncrementalBuilderState) (cache: TimeStampCache) slot fileInfo = + let createBoundModelGraphNode initialBoundModel (boundModels: ImmutableArray>.Builder) i = + let fileInfo = fileNames.[i] + let prevBoundModelGraphNode = + match i with + | 0 (* first file *) -> initialBoundModel + | _ -> boundModels.[i - 1] + let syntaxTree = GetSyntaxTree fileInfo + GraphNode(node { + let! prevBoundModel = prevBoundModelGraphNode.GetOrComputeValue() + return! TypeCheckTask enablePartialTypeChecking prevBoundModel syntaxTree + }) + + let rec createFinalizeBoundModelGraphNode (boundModels: ImmutableArray>.Builder) = + GraphNode(node { + // Compute last bound model then get all the evaluated models. + let! _ = boundModels.[boundModels.Count - 1].GetOrComputeValue() + let boundModels = + boundModels + |> Seq.map (fun x -> x.TryPeekValue().Value) + |> ImmutableArray.CreateRange + + let! result = FinalizeTypeCheckTask boundModels + let result = (result, DateTime.UtcNow) + return result + }) + + and computeStampedFileName (state: IncrementalBuilderState) (cache: TimeStampCache) slot fileInfo = let currentStamp = state.stampedFileNames.[slot] let stamp = StampFileNameTask cache fileInfo if currentStamp <> stamp then - match state.boundModels.[slot] with + match state.boundModels.[slot].TryPeekValue() with // This prevents an implementation file that has a backing signature file from invalidating the rest of the build. - | Some(boundModel) when state.enablePartialTypeChecking && boundModel.BackingSignature.IsSome -> - boundModel.Invalidate() + | ValueSome(boundModel) when enablePartialTypeChecking && boundModel.BackingSignature.IsSome -> + let newBoundModel = boundModel.ClearTcInfoExtras() { state with + boundModels = state.boundModels.RemoveAt(slot).Insert(slot, GraphNode(node { return newBoundModel })) stampedFileNames = state.stampedFileNames.SetItem(slot, StampFileNameTask cache fileInfo) } | _ -> @@ -968,11 +1018,11 @@ type IncrementalBuilder(tcGlobals, let stamp = StampFileNameTask cache fileNames.[slot + j] stampedFileNames.[slot + j] <- stamp logicalStampedFileNames.[slot + j] <- stamp - boundModels.[slot + j] <- None + boundModels.[slot + j] <- createBoundModelGraphNode state.initialBoundModel boundModels (slot + j) { state with // Something changed, the finalized view of the project must be invalidated. - finalizedBoundModel = None + finalizedBoundModel = createFinalizeBoundModelGraphNode boundModels stampedFileNames = stampedFileNames.ToImmutable() logicalStampedFileNames = logicalStampedFileNames.ToImmutable() @@ -981,7 +1031,7 @@ type IncrementalBuilder(tcGlobals, else state - let computeStampedFileNames state (cache: TimeStampCache) = + and computeStampedFileNames state (cache: TimeStampCache) = let mutable i = 0 (state, fileNames) ||> Array.fold (fun state fileInfo -> @@ -990,7 +1040,7 @@ type IncrementalBuilder(tcGlobals, newState ) - let computeStampedReferencedAssemblies state (cache: TimeStampCache) = + and computeStampedReferencedAssemblies state canTriggerInvalidation (cache: TimeStampCache) = let stampedReferencedAssemblies = state.stampedReferencedAssemblies.ToBuilder() let mutable referencesUpdated = false @@ -1006,84 +1056,18 @@ type IncrementalBuilder(tcGlobals, ) if referencesUpdated then - // Something changed, the finalized view of the project must be invalidated. - // This is the only place where the initial bound model will be invalidated. - let count = state.stampedFileNames.Length + // Build is invalidated. The build must be rebuilt with the newly updated references. + if not isImportsInvalidated && canTriggerInvalidation then + isImportsInvalidated <- true { state with stampedReferencedAssemblies = stampedReferencedAssemblies.ToImmutable() - initialBoundModel = None - finalizedBoundModel = None - stampedFileNames = Array.init count (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange - logicalStampedFileNames = Array.init count (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange - boundModels = Array.init count (fun _ -> None) |> ImmutableArray.CreateRange } else state - let computeInitialBoundModel (state: IncrementalBuilderState) (ctok: CompilationThreadToken) = - eventually { - match state.initialBoundModel with - | None -> - // Note this is not time-sliced - let! result = CombineImportedAssembliesTask ctok |> Eventually.ofCancellable - return { state with initialBoundModel = Some result }, result - | Some result -> - return state, result - } - - let computeBoundModel state (cache: TimeStampCache) (ctok: CompilationThreadToken) (slot: int) = - if IncrementalBuild.injectCancellationFault then Eventually.canceled() else - eventually { - - let fileInfo = fileNames.[slot] - - let state = computeStampedFileName state cache slot fileInfo - - if state.boundModels.[slot].IsNone then - let! (state, initial) = computeInitialBoundModel state ctok - - let prevBoundModel = - match slot with - | 0 (* first file *) -> initial - | _ -> - match state.boundModels.[slot - 1] with - | Some(prevBoundModel) -> prevBoundModel - | _ -> - // This shouldn't happen, but on the off-chance, just grab the initial bound model. - initial - - let! boundModel = TypeCheckTask ctok state.enablePartialTypeChecking prevBoundModel (ParseTask fileInfo) - - let state = - { state with - boundModels = state.boundModels.SetItem(slot, Some boundModel) - } - return state - - else - return state - } - - let computeBoundModels state (cache: TimeStampCache) (ctok: CompilationThreadToken) = - (state, [0..fileNames.Length-1]) ||> Eventually.fold (fun state slot -> computeBoundModel state cache ctok slot) - - let computeFinalizedBoundModel state (cache: TimeStampCache) (ctok: CompilationThreadToken) = - eventually { - let! state = computeBoundModels state cache ctok - - match state.finalizedBoundModel with - | Some result -> return state, result - | _ -> - let boundModels = state.boundModels |> Seq.choose id |> ImmutableArray.CreateRange - - let! result = FinalizeTypeCheckTask ctok state.enablePartialTypeChecking boundModels - let result = (result, DateTime.UtcNow) - return { state with finalizedBoundModel = Some result }, result - } - let tryGetSlot (state: IncrementalBuilderState) slot = - match state.boundModels.[slot] with - | Some boundModel -> + match state.boundModels.[slot].TryPeekValue() with + | ValueSome boundModel -> (boundModel, state.stampedFileNames.[slot]) |> Some | _ -> @@ -1092,39 +1076,18 @@ type IncrementalBuilder(tcGlobals, let tryGetBeforeSlot (state: IncrementalBuilderState) slot = match slot with | 0 (* first file *) -> - match state.initialBoundModel with - | Some initial -> - (initial, DateTime.MinValue) - |> Some - | _ -> - None + (initialBoundModel, DateTime.MinValue) + |> Some | _ -> tryGetSlot state (slot - 1) - let evalUpToTargetSlot state (cache: TimeStampCache) ctok targetSlot = - cancellable { - let state = computeStampedReferencedAssemblies state cache + let evalUpToTargetSlot (state: IncrementalBuilderState) targetSlot = + node { if targetSlot < 0 then - let! state, result = computeInitialBoundModel state ctok |> Eventually.toCancellable - return state, Some(result, DateTime.MinValue) + return Some(initialBoundModel, DateTime.MinValue) else - let! state = (state, [0..targetSlot]) ||> Eventually.fold (fun state slot -> computeBoundModel state cache ctok slot) |> Eventually.toCancellable - - let result = - state.boundModels.[targetSlot] - |> Option.map (fun boundModel -> - (boundModel, state.stampedFileNames.[targetSlot]) - ) - - return state, result - } - - let tryGetFinalized state cache ctok = - cancellable { - let state = computeStampedReferencedAssemblies state cache - - let! state, res = computeFinalizedBoundModel state cache ctok |> Eventually.toCancellable - return state, Some res + let! boundModel = state.boundModels.[targetSlot].GetOrComputeValue() + return Some(boundModel, state.stampedFileNames.[targetSlot]) } let MaxTimeStampInDependencies stamps = @@ -1143,19 +1106,44 @@ type IncrementalBuilder(tcGlobals, ReferencedAssembliesStamps => FileStamps => BoundModels => FinalizedBoundModel *) + let gate = obj () let mutable currentState = - { - stampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange - logicalStampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange - stampedReferencedAssemblies = Array.init referencedAssemblies.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange - initialBoundModel = None - boundModels = Array.zeroCreate fileNames.Length |> ImmutableArray.CreateRange - finalizedBoundModel = None - enablePartialTypeChecking = enablePartialTypeChecking - } + let cache = TimeStampCache(defaultTimeStamp) + let initialBoundModel = GraphNode(node { return initialBoundModel }) + let boundModels = ImmutableArray.CreateBuilder(fileNames.Length) + + for slot = 0 to fileNames.Length - 1 do + boundModels.Add(createBoundModelGraphNode initialBoundModel boundModels slot) + + let state = + { + stampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + logicalStampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + stampedReferencedAssemblies = Array.init referencedAssemblies.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + initialBoundModel = initialBoundModel + boundModels = boundModels.ToImmutable() + finalizedBoundModel = createFinalizeBoundModelGraphNode boundModels + } + let state = computeStampedReferencedAssemblies state false cache + let state = computeStampedFileNames state cache + state + + let computeProjectTimeStamp (state: IncrementalBuilderState) = + let t1 = MaxTimeStampInDependencies state.stampedReferencedAssemblies + let t2 = MaxTimeStampInDependencies state.logicalStampedFileNames + max t1 t2 + + let setCurrentState state cache (ct: CancellationToken) = + lock gate (fun () -> + ct.ThrowIfCancellationRequested() + currentState <- computeStampedFileNames state cache + ) - let setCurrentState (_ctok: CompilationThreadToken) state = - currentState <- state + let checkFileTimeStamps (cache: TimeStampCache) = + node { + let! ct = NodeCode.CancellationToken + setCurrentState currentState cache ct + } do IncrementalBuilderEventTesting.MRU.Add(IncrementalBuilderEventTesting.IBECreated) @@ -1173,20 +1161,20 @@ type IncrementalBuilder(tcGlobals, member _.ImportsInvalidatedByTypeProvider = importsInvalidatedByTypeProvider.Publish #endif + member _.IsReferencesInvalidated = + // fast path + if isImportsInvalidated then true + else + computeStampedReferencedAssemblies currentState true (TimeStampCache(defaultTimeStamp)) |> ignore + isImportsInvalidated + member _.AllDependenciesDeprecated = allDependencies - member _.PopulatePartialCheckingResults (ctok: CompilationThreadToken) = - eventually { + member _.PopulatePartialCheckingResults () = + node { let cache = TimeStampCache defaultTimeStamp // One per step - let state = currentState - let state = computeStampedFileNames state cache - setCurrentState ctok state - do! Eventually.ret () // allow cancellation - let state = computeStampedReferencedAssemblies state cache - setCurrentState ctok state - do! Eventually.ret () // allow cancellation - let! state, _res = computeFinalizedBoundModel state cache ctok - setCurrentState ctok state + do! checkFileTimeStamps cache + let! _ = currentState.finalizedBoundModel.GetOrComputeValue() projectChecked.Trigger() } @@ -1200,82 +1188,81 @@ type IncrementalBuilder(tcGlobals, member builder.TryGetCheckResultsBeforeFileInProject (filename) = let cache = TimeStampCache defaultTimeStamp - let state = currentState - let state = computeStampedFileNames state cache - let state = computeStampedReferencedAssemblies state cache + let tmpState = computeStampedFileNames currentState cache let slotOfFile = builder.GetSlotOfFileName filename - match tryGetBeforeSlot state slotOfFile with + match tryGetBeforeSlot tmpState slotOfFile with | Some(boundModel, timestamp) -> PartialCheckResults(boundModel, timestamp) |> Some | _ -> None member builder.AreCheckResultsBeforeFileInProjectReady filename = (builder.TryGetCheckResultsBeforeFileInProject filename).IsSome - member private _.GetCheckResultsBeforeSlotInProject (ctok: CompilationThreadToken, slotOfFile, enablePartialTypeChecking) = - cancellable { + member _.GetCheckResultsBeforeSlotInProject (slotOfFile) = + node { let cache = TimeStampCache defaultTimeStamp - let! state, result = evalUpToTargetSlot { currentState with enablePartialTypeChecking = enablePartialTypeChecking } cache ctok (slotOfFile - 1) - setCurrentState ctok { state with enablePartialTypeChecking = defaultPartialTypeChecking } + do! checkFileTimeStamps cache + let! result = evalUpToTargetSlot currentState (slotOfFile - 1) match result with | Some (boundModel, timestamp) -> return PartialCheckResults(boundModel, timestamp) - | None -> return! failwith "Build was not evaluated, expected the results to be ready after 'Eval' (GetCheckResultsBeforeSlotInProject)." + | None -> return! failwith "Expected results to be ready. (GetCheckResultsBeforeSlotInProject)." } - member builder.GetCheckResultsBeforeSlotInProject (ctok: CompilationThreadToken, slotOfFile) = - builder.GetCheckResultsBeforeSlotInProject(ctok, slotOfFile, defaultPartialTypeChecking) + member _.GetFullCheckResultsBeforeSlotInProject (slotOfFile) = + node { + let cache = TimeStampCache defaultTimeStamp + do! checkFileTimeStamps cache + let! result = evalUpToTargetSlot currentState (slotOfFile - 1) + match result with + | Some (boundModel, timestamp) -> + let! _ = boundModel.GetTcInfoExtras() + return PartialCheckResults(boundModel, timestamp) + | None -> return! failwith "Expected results to be ready. (GetFullCheckResultsBeforeSlotInProject)." + } - member builder.GetCheckResultsBeforeFileInProject (ctok: CompilationThreadToken, filename) = + member builder.GetCheckResultsBeforeFileInProject (filename) = let slotOfFile = builder.GetSlotOfFileName filename - builder.GetCheckResultsBeforeSlotInProject (ctok, slotOfFile) + builder.GetCheckResultsBeforeSlotInProject (slotOfFile) - member builder.GetCheckResultsAfterFileInProject (ctok: CompilationThreadToken, filename) = + member builder.GetCheckResultsAfterFileInProject (filename) = let slotOfFile = builder.GetSlotOfFileName filename + 1 - builder.GetCheckResultsBeforeSlotInProject (ctok, slotOfFile) + builder.GetCheckResultsBeforeSlotInProject (slotOfFile) + + member builder.GetFullCheckResultsBeforeFileInProject (filename) = + let slotOfFile = builder.GetSlotOfFileName filename + builder.GetFullCheckResultsBeforeSlotInProject (slotOfFile) - member builder.GetFullCheckResultsAfterFileInProject (ctok: CompilationThreadToken, filename) = - cancellable { + member builder.GetFullCheckResultsAfterFileInProject (filename) = + node { let slotOfFile = builder.GetSlotOfFileName filename + 1 - let! result = builder.GetCheckResultsBeforeSlotInProject(ctok, slotOfFile, false) - let! _ = result.GetTcInfoWithExtras() |> Eventually.toCancellable // Make sure we forcefully evaluate the info + let! result = builder.GetFullCheckResultsBeforeSlotInProject(slotOfFile) return result } - member builder.GetCheckResultsAfterLastFileInProject (ctok: CompilationThreadToken) = - builder.GetCheckResultsBeforeSlotInProject(ctok, builder.GetSlotsCount()) + member builder.GetCheckResultsAfterLastFileInProject () = + builder.GetCheckResultsBeforeSlotInProject(builder.GetSlotsCount()) - member private _.GetCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken, enablePartialTypeChecking) = - cancellable { - let cache = TimeStampCache defaultTimeStamp - - let! state, result = tryGetFinalized { currentState with enablePartialTypeChecking = enablePartialTypeChecking } cache ctok - setCurrentState ctok { state with enablePartialTypeChecking = defaultPartialTypeChecking } + member _.GetCheckResultsAndImplementationsForProject() = + node { + let cache = TimeStampCache(defaultTimeStamp) + do! checkFileTimeStamps cache + let! result = currentState.finalizedBoundModel.GetOrComputeValue() match result with - | Some ((ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt, boundModel), timestamp) -> + | ((ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt, boundModel), timestamp) -> return PartialCheckResults (boundModel, timestamp), ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt - | None -> - let msg = "Build was not evaluated, expected the results to be ready after 'tryGetFinalized')." - return! failwith msg } - member builder.GetCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken) = - builder.GetCheckResultsAndImplementationsForProject(ctok, defaultPartialTypeChecking) - - member builder.GetFullCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken) = - cancellable { - let! result = builder.GetCheckResultsAndImplementationsForProject(ctok, false) + member builder.GetFullCheckResultsAndImplementationsForProject() = + node { + let! result = builder.GetCheckResultsAndImplementationsForProject() let results, _, _, _ = result - let! _ = results.GetTcInfoWithExtras() |> Eventually.toCancellable // Make sure we forcefully evaluate the info + let! _ = results.GetTcInfoWithExtras() // Make sure we forcefully evaluate the info return result } member _.GetLogicalTimeStampForProject(cache) = - let state = currentState - let state = computeStampedFileNames state cache - let state = computeStampedReferencedAssemblies state cache - let t1 = MaxTimeStampInDependencies state.stampedReferencedAssemblies - let t2 = MaxTimeStampInDependencies state.logicalStampedFileNames - max t1 t2 + let tmpState = computeStampedFileNames currentState cache + computeProjectTimeStamp tmpState member _.TryGetSlotOfFileName(filename: string) = // Get the slot of the given file and force it to build. @@ -1300,9 +1287,9 @@ type IncrementalBuilder(tcGlobals, member builder.GetParseResultsForFile (filename) = let slotOfFile = builder.GetSlotOfFileName filename - let results = fileNames.[slotOfFile] + let fileInfo = fileNames.[slotOfFile] // re-parse on demand instead of retaining - let syntaxTree = ParseTask results + let syntaxTree = GetSyntaxTree fileInfo syntaxTree.Parse None member _.SourceFiles = sourceFiles |> List.map (fun (_, f, _) -> f) @@ -1310,7 +1297,7 @@ type IncrementalBuilder(tcGlobals, /// CreateIncrementalBuilder (for background type checking). Note that fsc.fs also /// creates an incremental builder used by the command line compiler. static member TryCreateIncrementalBuilderForProjectOptions - (ctok, legacyReferenceResolver, defaultFSharpBinariesDir, + (legacyReferenceResolver, defaultFSharpBinariesDir, frameworkTcImportsCache: FrameworkImportsCache, loadClosureOpt: LoadClosure option, sourceFiles: string list, @@ -1326,15 +1313,14 @@ type IncrementalBuilder(tcGlobals, let useSimpleResolutionSwitch = "--simpleresolution" - cancellable { + node { // Trap and report warnings and errors from creation. let delayedLogger = CapturingErrorLogger("IncrementalBuilderCreation") - use _unwindEL = PushErrorLoggerPhaseUntilUnwind (fun _ -> delayedLogger) - use _unwindBP = PushThreadBuildPhaseUntilUnwind BuildPhase.Parameter + use _ = new CompilationGlobalsScope(delayedLogger, BuildPhase.Parameter) let! builderOpt = - cancellable { + node { try // Create the builder. @@ -1342,7 +1328,7 @@ type IncrementalBuilder(tcGlobals, let resourceManager = new Lexhelp.LexResourceManager() /// Create a type-check configuration - let tcConfigB, sourceFilesNew = + let tcConfigB, sourceFiles = let getSwitchValue switchString = match commandLineArgs |> Seq.tryFindIndex(fun s -> s.StartsWithOrdinal switchString) with @@ -1409,39 +1395,41 @@ type IncrementalBuilder(tcGlobals, // script and its load closure to the configuration. // // NOTE: it would probably be cleaner and more accurate to re-run the load closure at this point. - match loadClosureOpt with - | Some loadClosure -> - let dllReferences = - [for reference in tcConfigB.referencedDLLs do - // If there's (one or more) resolutions of closure references then yield them all - match loadClosure.References |> List.tryFind (fun (resolved, _)->resolved=reference.Text) with - | Some (resolved, closureReferences) -> - for closureReference in closureReferences do - yield AssemblyReference(closureReference.originalReference.Range, resolved, None) - | None -> yield reference] - tcConfigB.referencedDLLs <- [] - tcConfigB.primaryAssembly <- (if loadClosure.UseDesktopFramework then PrimaryAssembly.Mscorlib else PrimaryAssembly.System_Runtime) - // Add one by one to remove duplicates - dllReferences |> List.iter (fun dllReference -> - tcConfigB.AddReferencedAssemblyByPath(dllReference.Range, dllReference.Text)) - tcConfigB.knownUnresolvedReferences <- loadClosure.UnresolvedReferences - | None -> () + let setupConfigFromLoadClosure () = + match loadClosureOpt with + | Some loadClosure -> + let dllReferences = + [for reference in tcConfigB.referencedDLLs do + // If there's (one or more) resolutions of closure references then yield them all + match loadClosure.References |> List.tryFind (fun (resolved, _)->resolved=reference.Text) with + | Some (resolved, closureReferences) -> + for closureReference in closureReferences do + yield AssemblyReference(closureReference.originalReference.Range, resolved, None) + | None -> yield reference] + tcConfigB.referencedDLLs <- [] + tcConfigB.primaryAssembly <- (if loadClosure.UseDesktopFramework then PrimaryAssembly.Mscorlib else PrimaryAssembly.System_Runtime) + // Add one by one to remove duplicates + dllReferences |> List.iter (fun dllReference -> + tcConfigB.AddReferencedAssemblyByPath(dllReference.Range, dllReference.Text)) + tcConfigB.knownUnresolvedReferences <- loadClosure.UnresolvedReferences + | None -> () + + setupConfigFromLoadClosure() let tcConfig = TcConfig.Create(tcConfigB, validate=true) let niceNameGen = NiceNameGenerator() - let outfile, _, assemblyName = tcConfigB.DecideNames sourceFilesNew + let outfile, _, assemblyName = tcConfigB.DecideNames sourceFiles // Resolve assemblies and create the framework TcImports. This is done when constructing the // builder itself, rather than as an incremental task. This caches a level of "system" references. No type providers are // included in these references. - let! (tcGlobals, frameworkTcImports, nonFrameworkResolutions, unresolvedReferences) = frameworkTcImportsCache.Get(ctok, tcConfig) + let! (tcGlobals, frameworkTcImports, nonFrameworkResolutions, unresolvedReferences) = frameworkTcImportsCache.Get(tcConfig) // Note we are not calling errorLogger.GetDiagnostics() anywhere for this task. // This is ok because not much can actually go wrong here. let errorOptions = tcConfig.errorSeverityOptions let errorLogger = CompilationErrorLogger("nonFrameworkAssemblyInputs", errorOptions) - // Return the disposable object that cleans up - use _holder = new CompilationGlobalsScope(errorLogger, BuildPhase.Parameter) + use _ = new CompilationGlobalsScope(errorLogger, BuildPhase.Parameter) // Get the names and time stamps of all the non-framework referenced assemblies, which will act // as inputs to one of the nodes in the build. @@ -1461,26 +1449,90 @@ type IncrementalBuilder(tcGlobals, for pr in projectReferences do yield Choice2Of2 pr, (fun (cache: TimeStampCache) -> cache.GetProjectReferenceTimeStamp (pr)) ] - let builder = - new IncrementalBuilder(tcGlobals, + // + // + // + // + // Start importing + + let tcConfigP = TcConfigProvider.Constant tcConfig + let beforeFileChecked = new Event() + let fileChecked = new Event() + +#if !NO_EXTENSIONTYPING + let importsInvalidatedByTypeProvider = new Event() +#endif + + // Check for the existence of loaded sources and prepend them to the sources list if present. + let sourceFiles = tcConfig.GetAvailableLoadedSources() @ (sourceFiles |>List.map (fun s -> rangeStartup, s)) + + // Mark up the source files with an indicator flag indicating if they are the last source file in the project + let sourceFiles = + let flags, isExe = tcConfig.ComputeCanContainEntryPoint(sourceFiles |> List.map snd) + ((sourceFiles, flags) ||> List.map2 (fun (m, nm) flag -> (m, nm, (flag, isExe)))) + + let basicDependencies = + [ for (UnresolvedAssemblyReference(referenceText, _)) in unresolvedReferences do + // Exclude things that are definitely not a file name + if not(FileSystem.IsInvalidPathShim referenceText) then + let file = if FileSystem.IsPathRootedShim referenceText then referenceText else Path.Combine(projectDirectory, referenceText) + yield file + + for r in nonFrameworkResolutions do + yield r.resolvedPath ] + + let allDependencies = + [| yield! basicDependencies + for (_, f, _) in sourceFiles do + yield f |] + + // For scripts, the dependency provider is already available. + // For projects create a fresh one for the project. + let dependencyProvider = + match dependencyProvider with + | None -> new DependencyProvider() + | Some dependencyProvider -> dependencyProvider + + let! initialBoundModel = + CombineImportedAssembliesTask( + assemblyName, + tcConfig, + tcConfigP, + tcGlobals, frameworkTcImports, - nonFrameworkAssemblyInputs, nonFrameworkResolutions, unresolvedReferences, - tcConfig, - projectDirectory, - outfile, - assemblyName, - niceNameGen, - resourceManager, - sourceFilesNew, + dependencyProvider, loadClosureOpt, + niceNameGen, + basicDependencies, keepAssemblyContents, keepAllBackgroundResolutions, keepAllBackgroundSymbolUses, enableBackgroundItemKeyStoreAndSemanticClassification, enablePartialTypeChecking, - dependencyProvider) + beforeFileChecked, + fileChecked, + importsInvalidatedByTypeProvider + ) + + let builder = + new IncrementalBuilder( + initialBoundModel, + tcGlobals, + nonFrameworkAssemblyInputs, + tcConfig, + outfile, + assemblyName, + resourceManager, + sourceFiles, + enablePartialTypeChecking, + beforeFileChecked, + fileChecked, +#if !NO_EXTENSIONTYPING + importsInvalidatedByTypeProvider, +#endif + allDependencies) return Some builder with e -> errorRecoveryNoRange e diff --git a/src/fsharp/service/IncrementalBuild.fsi b/src/fsharp/service/IncrementalBuild.fsi index bcaa09b942..5ffb0fb5c6 100755 --- a/src/fsharp/service/IncrementalBuild.fsi +++ b/src/fsharp/service/IncrementalBuild.fsi @@ -22,16 +22,17 @@ open FSharp.Compiler.Syntax open FSharp.Compiler.TcGlobals open FSharp.Compiler.Text open FSharp.Compiler.TypedTree +open FSharp.Compiler.BuildGraph /// Lookup the global static cache for building the FrameworkTcImports type internal FrameworkImportsCache = new : size: int -> FrameworkImportsCache - member Get : CompilationThreadToken * TcConfig -> Cancellable + member Get : TcConfig -> NodeCode - member Clear: CompilationThreadToken -> unit + member Clear: unit -> unit - member Downsize: CompilationThreadToken -> unit + member Downsize: unit -> unit /// Used for unit testing module internal IncrementalBuilderEventTesting = @@ -109,21 +110,21 @@ type internal PartialCheckResults = /// Compute the "TcInfo" part of the results. If `enablePartialTypeChecking` is false then /// extras will also be available. - member GetTcInfo: unit -> Eventually + member GetTcInfo: unit -> NodeCode /// Compute both the "TcInfo" and "TcInfoExtras" parts of the results. /// Can cause a second type-check if `enablePartialTypeChecking` is true in the checker. /// Only use when it's absolutely necessary to get rich information on a file. - member GetTcInfoWithExtras: unit -> Eventually + member GetTcInfoWithExtras: unit -> NodeCode /// Compute the "ItemKeyStore" parts of the results. /// Can cause a second type-check if `enablePartialTypeChecking` is true in the checker. /// Only use when it's absolutely necessary to get rich information on a file. - member TryGetItemKeyStore: unit -> Eventually + member TryGetItemKeyStore: unit -> NodeCode /// Can cause a second type-check if `enablePartialTypeChecking` is true in the checker. /// Only use when it's absolutely necessary to get rich information on a file. - member GetSemanticClassification: unit -> Eventually + member GetSemanticClassification: unit -> NodeCode member TimeStamp: DateTime @@ -153,15 +154,18 @@ type internal IncrementalBuilder = member ProjectChecked : IEvent #if !NO_EXTENSIONTYPING - /// Raised when a type provider invalidates the build. - member ImportsInvalidatedByTypeProvider : IEvent + /// Raised when the build is invalidated. + member ImportsInvalidatedByTypeProvider : IEvent #endif + /// Check if one of the build's references is invalidated. + member IsReferencesInvalidated : bool + /// The list of files the build depends on member AllDependenciesDeprecated : string[] /// The project build. Return true if the background work is finished. - member PopulatePartialCheckingResults: CompilationThreadToken -> Eventually + member PopulatePartialCheckingResults: unit -> NodeCode /// Get the preceding typecheck state of a slot, without checking if it is up-to-date w.r.t. /// the timestamps on files and referenced DLLs prior to this one. Return None if the result is not available. @@ -185,41 +189,34 @@ type internal IncrementalBuilder = /// Get the preceding typecheck state of a slot. Compute the entire type check of the project up /// to the necessary point if the result is not available. This may be a long-running operation. - /// - // TODO: make this an Eventually (which can be scheduled) or an Async (which can be cancelled) - member GetCheckResultsBeforeFileInProject : CompilationThreadToken * filename:string -> Cancellable + member GetCheckResultsBeforeFileInProject : filename:string -> NodeCode + + /// Get the preceding typecheck state of a slot. Compute the entire type check of the project up + /// to the necessary point if the result is not available. This may be a long-running operation. + /// This will get full type-check info for the file, meaning no partial type-checking. + member GetFullCheckResultsBeforeFileInProject : filename:string -> NodeCode /// Get the typecheck state after checking a file. Compute the entire type check of the project up /// to the necessary point if the result is not available. This may be a long-running operation. - /// - // TODO: make this an Eventually (which can be scheduled) or an Async (which can be cancelled) - member GetCheckResultsAfterFileInProject : CompilationThreadToken * filename:string -> Cancellable + member GetCheckResultsAfterFileInProject : filename:string -> NodeCode /// Get the typecheck state after checking a file. Compute the entire type check of the project up /// to the necessary point if the result is not available. This may be a long-running operation. /// This will get full type-check info for the file, meaning no partial type-checking. - /// - // TODO: make this an Eventually (which can be scheduled) or an Async (which can be cancelled) - member GetFullCheckResultsAfterFileInProject : CompilationThreadToken * filename:string -> Cancellable + member GetFullCheckResultsAfterFileInProject : filename:string -> NodeCode /// Get the typecheck result after the end of the last file. The typecheck of the project is not 'completed'. /// This may be a long-running operation. - /// - // TODO: make this an Eventually (which can be scheduled) or an Async (which can be cancelled) - member GetCheckResultsAfterLastFileInProject : CompilationThreadToken -> Cancellable + member GetCheckResultsAfterLastFileInProject : unit -> NodeCode /// Get the final typecheck result. If 'generateTypedImplFiles' was set on Create then the TypedAssemblyAfterOptimization will contain implementations. /// This may be a long-running operation. - /// - // TODO: make this an Eventually (which can be scheduled) or an Async (which can be cancelled) - member GetCheckResultsAndImplementationsForProject : CompilationThreadToken -> Cancellable + member GetCheckResultsAndImplementationsForProject : unit -> NodeCode /// Get the final typecheck result. If 'generateTypedImplFiles' was set on Create then the TypedAssemblyAfterOptimization will contain implementations. /// This may be a long-running operation. /// This will get full type-check info for the project, meaning no partial type-checking. - /// - // TODO: make this an Eventually (which can be scheduled) or an Async (which can be cancelled) - member GetFullCheckResultsAndImplementationsForProject : CompilationThreadToken -> Cancellable + member GetFullCheckResultsAndImplementationsForProject : unit -> NodeCode /// Get the logical time stamp that is associated with the output of the project if it were gully built immediately member GetLogicalTimeStampForProject: TimeStampCache -> DateTime @@ -234,7 +231,6 @@ type internal IncrementalBuilder = /// Create the incremental builder static member TryCreateIncrementalBuilderForProjectOptions: - CompilationThreadToken * LegacyReferenceResolver * defaultFSharpBinariesDir: string * FrameworkImportsCache * @@ -252,7 +248,7 @@ type internal IncrementalBuilder = enableBackgroundItemKeyStoreAndSemanticClassification: bool * enablePartialTypeChecking: bool * dependencyProvider: DependencyProvider option - -> Cancellable + -> NodeCode /// Generalized Incremental Builder. This is exposed only for unit testing purposes. module internal IncrementalBuild = diff --git a/src/fsharp/service/Reactor.fs b/src/fsharp/service/Reactor.fs deleted file mode 100755 index e8d8de2f40..0000000000 --- a/src/fsharp/service/Reactor.fs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. - -namespace FSharp.Compiler.CodeAnalysis - -open System -open System.Diagnostics -open System.Globalization -open System.Threading - -open Internal.Utilities.Library -open Internal.Utilities.Library.Extras - -/// Represents the capability to schedule work in the compiler service operations queue for the compilation thread -type internal IReactorOperations = - abstract EnqueueAndAwaitOpAsync : userOpName:string * opName:string * opArg:string * (CompilationThreadToken -> Cancellable<'T>) -> Async<'T> - abstract EnqueueOp: userOpName:string * opName:string * opArg:string * (CompilationThreadToken -> unit) -> unit - -[] -type internal ReactorCommands = - /// Kick off a build. - | SetBackgroundOp of ( (* userOpName: *) string * (* opName: *) string * (* opArg: *) string * (CompilationThreadToken -> Eventually)) option - - /// Do some work not synchronized in the mailbox. - | Op of userOpName: string * opName: string * opArg: string * CancellationToken * (CompilationThreadToken -> unit) * (unit -> unit) - - /// Finish the background building - | WaitForBackgroundOpCompletion of AsyncReplyChannel - - /// Finish all the queued ops - | CompleteAllQueuedOps of AsyncReplyChannel - -[] -/// There is one global Reactor for the entire language service, no matter how many projects or files -/// are open. -type Reactor() = - static let pauseBeforeBackgroundWorkDefault = GetEnvInteger "FCS_PauseBeforeBackgroundWorkMilliseconds" 10 - static let theReactor = Reactor() - let mutable pauseBeforeBackgroundWork = pauseBeforeBackgroundWorkDefault - - // We need to store the culture for the VS thread that is executing now, - // so that when the reactor picks up a thread from the thread pool we can set the culture - let mutable culture = CultureInfo(CultureInfo.CurrentUICulture.Name) - - let gate = obj() - let mutable bgOpCts = new CancellationTokenSource() - - let sw = new System.Diagnostics.Stopwatch() - - /// Maximum time share for a piece of background work before it should (cooperatively) yield - /// to enable other requests to be serviced. Yielding means returning a continuation function - /// (via an Eventually<_> value of case NotYetDone) that can be called as the next piece of work. - let maxTimeShareMilliseconds = - match System.Environment.GetEnvironmentVariable("FCS_MaxTimeShare") with - | null | "" -> 100L - | s -> int64 s - - /// Mailbox dispatch function. - let builder = - MailboxProcessor<_>.Start <| fun inbox -> - - // Async workflow which receives messages and dispatches to worker functions. - let rec loop (bgOpOpt, onComplete, bg) = - async { //Trace.TraceInformation("Reactor: receiving..., remaining {0}", inbox.CurrentQueueLength) - - // Explanation: The reactor thread acts as the compilation thread in hosted scenarios - let ctok = AssumeCompilationThreadWithoutEvidence() - - // Messages always have priority over the background op. - let! msg = - async { match bgOpOpt, onComplete with - | None, None -> - let! msg = inbox.Receive() - return Some msg - | _, Some _ -> - return! inbox.TryReceive(0) - | Some _, _ -> - let timeout = - if bg then 0 - else - Trace.TraceInformation("Reactor: {0:n3} pausing {1} milliseconds", DateTime.Now.TimeOfDay.TotalSeconds, pauseBeforeBackgroundWork) - pauseBeforeBackgroundWork - return! inbox.TryReceive(timeout) } - Thread.CurrentThread.CurrentUICulture <- culture - match msg with - | Some (SetBackgroundOp bgOpOpt) -> - let bgOpOpt = - match bgOpOpt with - | None -> None - | Some (bgUserOpName, bgOpName, bgOpArg, bgOp) -> - lock gate (fun () -> - let oldBgOpCts = bgOpCts - bgOpCts <- new CancellationTokenSource() - oldBgOpCts.Dispose() - ) - Some (bgUserOpName, bgOpName, bgOpArg, bgOp ctok) - - //Trace.TraceInformation("Reactor: --> set background op, remaining {0}", inbox.CurrentQueueLength) - return! loop (bgOpOpt, onComplete, false) - - | Some (Op (userOpName, opName, opArg, ct, op, ccont)) -> - if ct.IsCancellationRequested then ccont() else - Trace.TraceInformation("Reactor: {0:n3} --> {1}.{2} ({3}), remaining {4}", DateTime.Now.TimeOfDay.TotalSeconds, userOpName, opName, opArg, inbox.CurrentQueueLength) - let time = Stopwatch() - time.Start() - op ctok - time.Stop() - let span = time.Elapsed - //if span.TotalMilliseconds > 100.0 then - let taken = span.TotalMilliseconds - let msg = (if taken > 10000.0 then "BAD-OP: >10s " elif taken > 3000.0 then "BAD-OP: >3s " elif taken > 1000.0 then "BAD-OP: > 1s " elif taken > 500.0 then "BAD-OP: >0.5s " else "") - Trace.TraceInformation("Reactor: {0:n3} {1}<-- {2}.{3}, took {4} ms", DateTime.Now.TimeOfDay.TotalSeconds, msg, userOpName, opName, span.TotalMilliseconds) - return! loop (bgOpOpt, onComplete, false) - - | Some (WaitForBackgroundOpCompletion channel) -> - match bgOpOpt with - | None -> () - | Some (bgUserOpName, bgOpName, bgOpArg, bgOp) -> - Trace.TraceInformation("Reactor: {0:n3} --> wait for background {1}.{2} ({3}), remaining {4}", DateTime.Now.TimeOfDay.TotalSeconds, bgUserOpName, bgOpName, bgOpArg, inbox.CurrentQueueLength) - lock gate (fun () -> - let oldBgOpCts = bgOpCts - bgOpCts <- new CancellationTokenSource() - oldBgOpCts.Dispose() - ) - - try - Eventually.force bgOpCts.Token bgOp |> ignore - with :? OperationCanceledException -> () - - if bgOpCts.IsCancellationRequested then - Trace.TraceInformation("FCS: <-- wait for background was cancelled {0}.{1}", bgUserOpName, bgOpName) - - channel.Reply(()) - return! loop (None, onComplete, false) - - | Some (CompleteAllQueuedOps channel) -> - Trace.TraceInformation("Reactor: {0:n3} --> stop background work and complete all queued ops, remaining {1}", DateTime.Now.TimeOfDay.TotalSeconds, inbox.CurrentQueueLength) - return! loop (None, Some channel, false) - - | None -> - match bgOpOpt, onComplete with - | _, Some onComplete -> onComplete.Reply() - | Some (bgUserOpName, bgOpName, bgOpArg, bgEv), None -> - Trace.TraceInformation("Reactor: {0:n3} --> background step {1}.{2} ({3})", DateTime.Now.TimeOfDay.TotalSeconds, bgUserOpName, bgOpName, bgOpArg) - - // Force for a timeslice. If cancellation occurs we abandon the background work. - let bgOpRes = - match Eventually.forceForTimeSlice sw maxTimeShareMilliseconds bgOpCts.Token bgEv with - | ValueOrCancelled.Value cont -> cont - | ValueOrCancelled.Cancelled _ -> Eventually.Done () - - let bgOp2 = - match bgOpRes with - | _ when bgOpCts.IsCancellationRequested -> - Trace.TraceInformation("FCS: <-- background step {0}.{1}, was cancelled", bgUserOpName, bgOpName) - None - | Eventually.Done () -> None - | bgEv2 -> Some (bgUserOpName, bgOpName, bgOpArg, bgEv2) - - //if span.TotalMilliseconds > 100.0 then - //let msg = (if taken > 10000.0 then "BAD-BG-SLICE: >10s " elif taken > 3000.0 then "BAD-BG-SLICE: >3s " elif taken > 1000.0 then "BAD-BG-SLICE: > 1s " else "") - //Trace.TraceInformation("Reactor: {0:n3} {1}<-- background step, took {2}ms", DateTime.Now.TimeOfDay.TotalSeconds, msg, taken) - return! loop (bgOp2, onComplete, true) - | None, None -> failwith "unreachable, should have used inbox.Receive" - } - async { - while true do - try - do! loop (None, None, false) - with e -> - Debug.Assert(false, String.Format("unexpected failure in reactor loop {0}, restarting", e)) - } - - member _.SetPreferredUILang(preferredUiLang: string option) = - match preferredUiLang with - | Some s -> - culture <- CultureInfo s -#if FX_RESHAPED_GLOBALIZATION - CultureInfo.CurrentUICulture <- culture -#else - Thread.CurrentThread.CurrentUICulture <- culture -#endif - | None -> () - - // [Foreground Mailbox Accessors] ----------------------------------------------------------- - member _.SetBackgroundOp(bgOpOpt) = - Trace.TraceInformation("Reactor: {0:n3} enqueue start background, length {1}", DateTime.Now.TimeOfDay.TotalSeconds, builder.CurrentQueueLength) - lock gate (fun () -> bgOpCts.Cancel()) - builder.Post(SetBackgroundOp bgOpOpt) - - member _.CancelBackgroundOp() = - Trace.TraceInformation("FCS: trying to cancel any active background work") - lock gate (fun () -> bgOpCts.Cancel()) - - member _.EnqueueOp(userOpName, opName, opArg, op) = - Trace.TraceInformation("Reactor: {0:n3} enqueue {1}.{2} ({3}), length {4}", DateTime.Now.TimeOfDay.TotalSeconds, userOpName, opName, opArg, builder.CurrentQueueLength) - builder.Post(Op(userOpName, opName, opArg, CancellationToken.None, op, (fun () -> ()))) - - member _.EnqueueOpPrim(userOpName, opName, opArg, ct, op, ccont) = - Trace.TraceInformation("Reactor: {0:n3} enqueue {1}.{2} ({3}), length {4}", DateTime.Now.TimeOfDay.TotalSeconds, userOpName, opName, opArg, builder.CurrentQueueLength) - builder.Post(Op(userOpName, opName, opArg, ct, op, ccont)) - - member _.CurrentQueueLength = - builder.CurrentQueueLength - - // This is for testing only - member _.WaitForBackgroundOpCompletion() = - Trace.TraceInformation("Reactor: {0:n3} enqueue wait for background, length {1}", DateTime.Now.TimeOfDay.TotalSeconds, builder.CurrentQueueLength) - builder.PostAndReply WaitForBackgroundOpCompletion - - // This is for testing only - member _.CompleteAllQueuedOps() = - Trace.TraceInformation("Reactor: {0:n3} enqueue wait for all ops, length {1}", DateTime.Now.TimeOfDay.TotalSeconds, builder.CurrentQueueLength) - builder.PostAndReply CompleteAllQueuedOps - - member r.EnqueueAndAwaitOpAsync (userOpName, opName, opArg, f) = - async { - let! ct = Async.CancellationToken - let resultCell = AsyncUtil.AsyncResultCell<_>() - r.EnqueueOpPrim(userOpName, opName, opArg, ct, - op=(fun ctok -> - let result = - try - match Cancellable.run ct (f ctok) with - | ValueOrCancelled.Value r -> AsyncUtil.AsyncOk r - | ValueOrCancelled.Cancelled e -> AsyncUtil.AsyncCanceled e - with e -> e |> AsyncUtil.AsyncException - - resultCell.RegisterResult(result)), - ccont=(fun () -> resultCell.RegisterResult (AsyncUtil.AsyncCanceled(OperationCanceledException(ct))) ) - - ) - return! resultCell.AsyncResult - } - - member _.PauseBeforeBackgroundWork with get() = pauseBeforeBackgroundWork and set v = pauseBeforeBackgroundWork <- v - - static member Singleton = theReactor - diff --git a/src/fsharp/service/Reactor.fsi b/src/fsharp/service/Reactor.fsi deleted file mode 100755 index f17caccc0e..0000000000 --- a/src/fsharp/service/Reactor.fsi +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. - -namespace FSharp.Compiler.CodeAnalysis - -open System.Threading -open Internal.Utilities.Library - -/// Represents the capability to schedule work in the compiler service operations queue for the compilation thread -type internal IReactorOperations = - - /// Put the operation in the queue, and return an async handle to its result. - abstract EnqueueAndAwaitOpAsync : userOpName:string * opName:string * opArg:string * action: (CompilationThreadToken -> Cancellable<'T>) -> Async<'T> - - /// Enqueue an operation and return immediately. - abstract EnqueueOp: userOpName:string * opName:string * opArg:string * action: (CompilationThreadToken -> unit) -> unit - -/// Reactor is intended for long-running but interruptible operations, interleaved -/// with one-off asynchronous operations. -/// -/// It is used to guard the global compiler state while maintaining responsiveness on -/// the UI thread. -/// Reactor operations -[] -type internal Reactor = - - /// Allows to specify the language for error messages - member SetPreferredUILang : string option -> unit - - /// Set the background building function, which is called repeatedly - /// until it returns 'false'. If None then no background operation is used. - /// The operation is an Eventually which can be run in time slices. - member SetBackgroundOp : ( (* userOpName:*) string * (* opName: *) string * (* opArg: *) string * (CompilationThreadToken -> Eventually)) option -> unit - - /// Cancel any work being don by the background building function. - member CancelBackgroundOp : unit -> unit - - /// Block until the current implicit background build is complete. Unit test only. - member WaitForBackgroundOpCompletion : unit -> unit - - /// Block until all operations in the queue are complete - member CompleteAllQueuedOps : unit -> unit - - /// Enqueue an uncancellable operation and return immediately. - member EnqueueOp : userOpName:string * opName: string * opArg: string * op:(CompilationThreadToken -> unit) -> unit - - /// For debug purposes - member CurrentQueueLength : int - - /// Put the operation in the queue, and return an async handle to its result. - member EnqueueAndAwaitOpAsync : userOpName:string * opName:string * opArg:string * (CompilationThreadToken -> Cancellable<'T>) -> Async<'T> - - /// The timespan in milliseconds before background work begins after the operations queue is empty - member PauseBeforeBackgroundWork : int with get, set - - /// Get the reactor - static member Singleton : Reactor - diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 4a65f6ca1a..46bcc86ff7 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -7,6 +7,7 @@ open System.Collections.Concurrent open System.Diagnostics open System.IO open System.Reflection +open System.Threading open Internal.Utilities.Collections open Internal.Utilities.Library open Internal.Utilities.Library.Extras @@ -32,6 +33,7 @@ open FSharp.Compiler.Tokenization open FSharp.Compiler.Text open FSharp.Compiler.Text.Range open FSharp.Compiler.TcGlobals +open FSharp.Compiler.BuildGraph [] module EnvMisc = @@ -139,7 +141,7 @@ module CompileHelpers = errors.ToArray(), result - let createDynamicAssembly (ctok, debugInfo: bool, tcImportsRef: TcImports option ref, execute: bool, assemblyBuilderRef: _ option ref) (tcConfig: TcConfig, tcGlobals:TcGlobals, outfile, ilxMainModule) = + let createDynamicAssembly (debugInfo: bool, tcImportsRef: TcImports option ref, execute: bool, assemblyBuilderRef: _ option ref) (tcConfig: TcConfig, tcGlobals:TcGlobals, outfile, ilxMainModule) = // Create an assembly builder let assemblyName = System.Reflection.AssemblyName(System.IO.Path.GetFileNameWithoutExtension outfile) @@ -162,7 +164,7 @@ module CompileHelpers = // The function used to resolve types while emitting the code let assemblyResolver s = - match tcImportsRef.Value.Value.TryFindExistingFullyQualifiedPathByExactAssemblyRef (ctok, s) with + match tcImportsRef.Value.Value.TryFindExistingFullyQualifiedPathByExactAssemblyRef (s) with | Some res -> Some (Choice1Of2 res) | None -> None @@ -204,23 +206,26 @@ type FileVersion = int type ParseCacheLockToken() = interface LockToken type ScriptClosureCacheToken() = interface LockToken +type CheckFileCacheKey = FileName * SourceTextHash * FSharpProjectOptions +type CheckFileCacheValue = FSharpParseFileResults * FSharpCheckFileResults * SourceTextHash * DateTime // There is only one instance of this type, held in FSharpChecker -type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyContents, keepAllBackgroundResolutions, tryGetMetadataSnapshot, suggestNamesForErrors, keepAllBackgroundSymbolUses, enableBackgroundItemKeyStoreAndSemanticClassification, enablePartialTypeChecking) as self = - // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.backgroundCompiler.reactor: The one and only Reactor - let reactor = Reactor.Singleton +type BackgroundCompiler( + legacyReferenceResolver, + projectCacheSize, + keepAssemblyContents, + keepAllBackgroundResolutions, + tryGetMetadataSnapshot, + suggestNamesForErrors, + keepAllBackgroundSymbolUses, + enableBackgroundItemKeyStoreAndSemanticClassification, + enablePartialTypeChecking) as self = + let beforeFileChecked = Event() let fileParsed = Event() let fileChecked = Event() let projectChecked = Event() - - let mutable implicitlyStartBackgroundWork = true - let reactorOps = - { new IReactorOperations with - member _.EnqueueAndAwaitOpAsync (userOpName, opName, opArg, op) = reactor.EnqueueAndAwaitOpAsync (userOpName, opName, opArg, op) - member _.EnqueueOp (userOpName, opName, opArg, op) = reactor.EnqueueOp (userOpName, opName, opArg, op) } - // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.backgroundCompiler.scriptClosureCache /// Information about the derived script closure. let scriptClosureCache = @@ -240,8 +245,8 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC /// CreateOneIncrementalBuilder (for background type checking). Note that fsc.fs also /// creates an incremental builder used by the command line compiler. - let CreateOneIncrementalBuilder (ctok, options:FSharpProjectOptions, userOpName) = - cancellable { + let CreateOneIncrementalBuilder (options:FSharpProjectOptions, userOpName) = + node { Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CreateOneIncrementalBuilder", options.ProjectFileName) let projectReferences = [ for r in options.ReferencedProjects do @@ -256,10 +261,10 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC yield { new IProjectReference with - member x.EvaluateRawContents(ctok) = - cancellable { + member x.EvaluateRawContents() = + node { Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "GetAssemblyData", nm) - return! self.GetAssemblyData(opts, ctok, userOpName + ".CheckReferencedProject("+nm+")") + return! self.GetAssemblyData(opts, userOpName + ".CheckReferencedProject("+nm+")") } member x.TryGetLogicalTimeStamp(cache) = self.TryGetLogicalTimeStampForProject(cache, opts) @@ -268,9 +273,9 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC | FSharpReferencedProject.PEReference(nm,stamp,delayedReader) -> yield { new IProjectReference with - member x.EvaluateRawContents(_) = - cancellable { - let! ilReaderOpt = delayedReader.TryGetILModuleReader() + member x.EvaluateRawContents() = + node { + let! ilReaderOpt = delayedReader.TryGetILModuleReader() |> NodeCode.FromCancellable match ilReaderOpt with | Some ilReader -> let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs @@ -284,8 +289,8 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC | FSharpReferencedProject.ILModuleReference(nm,getStamp,getReader) -> yield { new IProjectReference with - member x.EvaluateRawContents(_) = - cancellable { + member x.EvaluateRawContents() = + node { let ilReader = getReader() let ilModuleDef, ilAsmRefs = ilReader.ILModuleDef, ilReader.ILAssemblyRefs return RawFSharpAssemblyData(ilModuleDef, ilAsmRefs) :> IRawFSharpAssemblyData |> Some @@ -298,7 +303,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC let! builderOpt, diagnostics = IncrementalBuilder.TryCreateIncrementalBuilderForProjectOptions - (ctok, legacyReferenceResolver, FSharpCheckerResultsSettings.defaultFSharpBinariesDir, frameworkTcImportsCache, loadClosure, Array.toList options.SourceFiles, + (legacyReferenceResolver, FSharpCheckerResultsSettings.defaultFSharpBinariesDir, frameworkTcImportsCache, loadClosure, Array.toList options.SourceFiles, Array.toList options.OtherOptions, projectReferences, options.ProjectDirectory, options.UseScriptResolutionRules, keepAssemblyContents, keepAllBackgroundResolutions, tryGetMetadataSnapshot, suggestNamesForErrors, keepAllBackgroundSymbolUses, @@ -313,8 +318,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC #if !NO_EXTENSIONTYPING // Register the behaviour that responds to CCUs being invalidated because of type // provider Invalidate events. This invalidates the configuration in the build. - builder.ImportsInvalidatedByTypeProvider.Add (fun _ -> - self.InvalidateConfiguration(options, None, userOpName)) + builder.ImportsInvalidatedByTypeProvider.Add(fun () -> self.InvalidateConfiguration(options, userOpName)) #endif // Register the callback called just before a file is typechecked by the background builder (without recording @@ -334,117 +338,140 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC // live information than anything else in the F# Language Service, since it holds up to 3 (projectCacheStrongSize) background project builds // strongly. // - /// Cache of builds keyed by options. + /// Cache of builds keyed by options. + let gate = obj() let incrementalBuildersCache = - MruCache + MruCache> (keepStrongly=projectCacheSize, keepMax=projectCacheSize, areSame = FSharpProjectOptions.AreSameForChecking, areSimilar = FSharpProjectOptions.UseSameProject) - let tryGetBuilder options = + let tryGetBuilderNode options = incrementalBuildersCache.TryGet (AnyCallerThread, options) - let tryGetSimilarBuilder options = + let tryGetBuilder options : NodeCode<(IncrementalBuilder option * FSharpDiagnostic[])> option = + tryGetBuilderNode options + |> Option.map (fun x -> x.GetOrComputeValue()) + + let tryGetSimilarBuilder options : NodeCode<(IncrementalBuilder option * FSharpDiagnostic[])> option = incrementalBuildersCache.TryGetSimilar (AnyCallerThread, options) + |> Option.map (fun x -> x.GetOrComputeValue()) - let tryGetAnyBuilder options = + let tryGetAnyBuilder options : NodeCode<(IncrementalBuilder option * FSharpDiagnostic[])> option = incrementalBuildersCache.TryGetAny (AnyCallerThread, options) + |> Option.map (fun x -> x.GetOrComputeValue()) + + let createBuilderNode (options, userOpName, ct: CancellationToken) = + lock gate (fun () -> + if ct.IsCancellationRequested then + GraphNode(node { return None, [||] }) + else + let getBuilderNode = + GraphNode(CreateOneIncrementalBuilder(options, userOpName)) + incrementalBuildersCache.Set (AnyCallerThread, options, getBuilderNode) + getBuilderNode + ) - let getOrCreateBuilder (ctok, options, userOpName) = - cancellable { - match tryGetBuilder options with - | Some (builderOpt,creationDiags) -> - Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache - return builderOpt,creationDiags - | None -> - Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_BuildingNewCache - let! (builderOpt,creationDiags) as info = CreateOneIncrementalBuilder (ctok, options, userOpName) - incrementalBuildersCache.Set (AnyCallerThread, options, info) - return builderOpt, creationDiags - } + let createAndGetBuilder (options, userOpName) = + node { + let! ct = NodeCode.CancellationToken + let getBuilderNode = createBuilderNode (options, userOpName, ct) + return! getBuilderNode.GetOrComputeValue() + } - let getSimilarOrCreateBuilder (ctok, options, userOpName) = - RequireCompilationThread ctok + let getOrCreateBuilder (options, userOpName) : NodeCode<(IncrementalBuilder option * FSharpDiagnostic[])> = + match tryGetBuilder options with + | Some getBuilder -> + node { + match! getBuilder with + | builderOpt, creationDiags when builderOpt.IsNone || not builderOpt.Value.IsReferencesInvalidated -> + Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache + return builderOpt,creationDiags + | _ -> + return! createAndGetBuilder (options, userOpName) + } + | _ -> + createAndGetBuilder (options, userOpName) + + let getSimilarOrCreateBuilder (options, userOpName) = match tryGetSimilarBuilder options with - | Some res -> Cancellable.ret res + | Some res -> res // The builder does not exist at all. Create it. - | None -> getOrCreateBuilder (ctok, options, userOpName) + | None -> getOrCreateBuilder (options, userOpName) - let getOrCreateBuilderWithInvalidationFlag (ctok, options, canInvalidateProject, userOpName) = + let getOrCreateBuilderWithInvalidationFlag (options, canInvalidateProject, userOpName) = if canInvalidateProject then - getOrCreateBuilder (ctok, options, userOpName) + getOrCreateBuilder (options, userOpName) else - getSimilarOrCreateBuilder (ctok, options, userOpName) + getSimilarOrCreateBuilder (options, userOpName) - let getAnyBuilder (reactor: Reactor) (options, userOpName, opName, opArg) = - let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, opName, opArg, action) + let getAnyBuilder (options, userOpName) = match tryGetAnyBuilder options with - | Some (builderOpt,creationDiags) -> + | Some getBuilder -> Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache - async { return builderOpt,creationDiags } + getBuilder | _ -> - execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) - - let getBuilder (reactor: Reactor) (options, userOpName, opName, opArg) = - let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, opName, opArg, action) - match tryGetBuilder options with - | Some (builderOpt,creationDiags) -> - Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache - async { return builderOpt,creationDiags } - | _ -> - execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) + getOrCreateBuilder (options, userOpName) let parseCacheLock = Lock() // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.parseFileInProjectCache. Most recently used cache for parsing files. let parseFileCache = MruCache(parseFileCacheSize, areSimilar = AreSimilarForParsing, areSame = AreSameForParsing) - // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.checkFileInProjectCachePossiblyStale // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.checkFileInProjectCache // /// Cache which holds recently seen type-checks. /// This cache may hold out-of-date entries, in two senses /// - there may be a more recent antecedent state available because the background build has made it available /// - the source for the file may have changed - - let checkFileInProjectCachePossiblyStale = - MruCache - (keepStrongly=checkFileInProjectCacheSize, - areSame=AreSameForChecking2, - areSimilar=AreSubsumable2) // Also keyed on source. This can only be out of date if the antecedent is out of date - let checkFileInProjectCache = - MruCache + let checkFileInProjectCache = + MruCache> (keepStrongly=checkFileInProjectCacheSize, areSame=AreSameForChecking3, areSimilar=AreSubsumable3) - /// Holds keys for files being currently checked. It's used to prevent checking same file in parallel (interleaving chunk queued to Reactor). - let beingCheckedFileTable = - ConcurrentDictionary - (HashIdentity.FromFunctions - hash - (fun (f1, o1, v1) (f2, o2, v2) -> f1 = f2 && v1 = v2 && FSharpProjectOptions.AreSameForChecking(o1, o2))) + /// Should be a fast operation. Ensures that we have only one async lazy object per file and its hash. + let getCheckFileNode (parseResults, + sourceText, + fileName, + options, + _fileVersion, + builder, + tcPrior, + tcInfo, + creationDiags) (onComplete) = + + // Here we lock for the creation of the node, not its execution + parseCacheLock.AcquireLock (fun ltok -> + let key = (fileName, sourceText.GetHashCode() |> int64, options) + match checkFileInProjectCache.TryGet(ltok, key) with + | Some res -> res + | _ -> + let res = + GraphNode(node { + let! res = + self.CheckOneFileImplAux( + parseResults, + sourceText, + fileName, + options, + builder, + tcPrior, + tcInfo, + creationDiags) + onComplete() + return res + }) + checkFileInProjectCache.Set(ltok, key, res) + res + ) static let mutable actualParseFileCount = 0 static let mutable actualCheckFileCount = 0 - member _.RecordCheckFileInProjectResults(filename,options,parsingOptions,parseResults,fileVersion,priorTimeStamp,checkAnswer,hash) = - match checkAnswer with - | None -> () - | Some typedResults -> - actualCheckFileCount <- actualCheckFileCount + 1 - parseCacheLock.AcquireLock (fun ltok -> - checkFileInProjectCachePossiblyStale.Set(ltok, (filename,options),(parseResults,typedResults,fileVersion)) - checkFileInProjectCache.Set(ltok, (filename, hash, options),(parseResults,typedResults,fileVersion,priorTimeStamp)) - parseFileCache.Set(ltok, (filename, hash, parsingOptions), parseResults)) - - member bc.ImplicitlyStartCheckProjectInBackground(options, userOpName) = - if implicitlyStartBackgroundWork then - bc.CheckProjectInBackground(options, userOpName + ".ImplicitlyStartCheckProjectInBackground") - member _.ParseFile(filename: string, sourceText: ISourceText, options: FSharpParsingOptions, cache: bool, userOpName: string) = async { if cache then @@ -452,7 +479,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC match parseCacheLock.AcquireLock(fun ltok -> parseFileCache.TryGet(ltok, (filename, hash, options))) with | Some res -> return res | None -> - actualParseFileCount <- actualParseFileCount + 1 + Interlocked.Increment(&actualParseFileCount) |> ignore let parseDiags, parseTree, anyErrors = ParseAndCheckFile.parseFile(sourceText, filename, options, userOpName, suggestNamesForErrors) let res = FSharpParseFileResults(parseDiags, parseTree, anyErrors, options.SourceFiles) parseCacheLock.AcquireLock(fun ltok -> parseFileCache.Set(ltok, (filename, hash, options), res)) @@ -464,8 +491,8 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC /// Fetch the parse information from the background compiler (which checks w.r.t. the FileSystem API) member _.GetBackgroundParseResultsForFileInProject(filename, options, userOpName) = - async { - let! builderOpt, creationDiags = getBuilder reactor (options, userOpName, "GetBackgroundParseResultsForFileInProject ", filename) + node { + let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with | None -> let parseTree = EmptyParsedInput(filename, (false, false)) @@ -477,42 +504,71 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC } member _.GetCachedCheckFileResult(builder: IncrementalBuilder, filename, sourceText: ISourceText, options) = - let hash = sourceText.GetHashCode() |> int64 - - // Check the cache. We can only use cached results when there is no work to do to bring the background builder up-to-date - let cachedResults = parseCacheLock.AcquireLock (fun ltok -> checkFileInProjectCache.TryGet(ltok, (filename, hash, options))) - - let result = - match cachedResults with - | Some (parseResults, checkResults,_,priorTimeStamp) - when - (match builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename with - | None -> false - | Some(tcPrior) -> - tcPrior.TimeStamp = priorTimeStamp && - builder.AreCheckResultsBeforeFileInProjectReady(filename)) -> - Some (parseResults,checkResults) + node { + let hash = sourceText.GetHashCode() |> int64 + let key = (filename, hash, options) + let cachedResultsOpt = parseCacheLock.AcquireLock(fun ltok -> checkFileInProjectCache.TryGet(ltok, key)) + + match cachedResultsOpt with + | Some cachedResults -> + match! cachedResults.GetOrComputeValue() with + | (parseResults, checkResults,_,priorTimeStamp) + when + (match builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename with + | None -> false + | Some(tcPrior) -> + tcPrior.TimeStamp = priorTimeStamp && + builder.AreCheckResultsBeforeFileInProjectReady(filename)) -> + return Some (parseResults,checkResults) + | _ -> + parseCacheLock.AcquireLock(fun ltok -> checkFileInProjectCache.RemoveAnySimilar(ltok, key)) + return None | _ -> - None + return None + } + + member private bc.CheckOneFileImplAux + (parseResults: FSharpParseFileResults, + sourceText: ISourceText, + fileName: string, + options: FSharpProjectOptions, + builder: IncrementalBuilder, + tcPrior: PartialCheckResults, + tcInfo: TcInfo, + creationDiags: FSharpDiagnostic[]) : NodeCode = + + node { + // Get additional script #load closure information if applicable. + // For scripts, this will have been recorded by GetProjectOptionsFromScript. + let tcConfig = tcPrior.TcConfig + let loadClosure = scriptClosureCache.TryGet(AnyCallerThread, options) + + let! checkAnswer = + FSharpCheckFileResults.CheckOneFile + (parseResults, + sourceText, + fileName, + options.ProjectFileName, + tcConfig, + tcPrior.TcGlobals, + tcPrior.TcImports, + tcInfo.tcState, + tcInfo.moduleNamesDict, + loadClosure, + tcInfo.TcErrors, + options.IsIncompleteTypeCheckEnvironment, + options, + builder, + Array.ofList tcInfo.tcDependencyFiles, + creationDiags, + parseResults.Diagnostics, + keepAssemblyContents, + suggestNamesForErrors) |> NodeCode.FromCancellable + GraphNode.SetPreferredUILang tcConfig.preferredUiLang + return (parseResults, checkAnswer, sourceText.GetHashCode() |> int64, tcPrior.TimeStamp) + } + - if result.IsNone then - parseCacheLock.AcquireLock (fun ltok -> checkFileInProjectCache.RemoveAnySimilar(ltok, (filename, hash, options))) - - result - - /// 1. Repeatedly try to get cached file check results or get file "lock". - /// - /// 2. If it've got cached results, returns them. - /// - /// 3. If it've not got the lock for 1 minute, returns `FSharpCheckFileAnswer.Aborted`. - /// - /// 4. Type checks the file. - /// - /// 5. Records results in `BackgroundCompiler` caches. - /// - /// 6. Starts whole project background compilation. - /// - /// 7. Releases the file "lock". member private bc.CheckOneFileImpl (parseResults: FSharpParseFileResults, sourceText: ISourceText, @@ -522,207 +578,136 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC builder: IncrementalBuilder, tcPrior: PartialCheckResults, tcInfo: TcInfo, - creationDiags: FSharpDiagnostic[]) = - - async { - let beingCheckedFileKey = fileName, options, fileVersion - let stopwatch = Stopwatch.StartNew() - let rec loop() = - async { - // results may appear while we were waiting for the lock, let's recheck if it's the case - let cachedResults = bc.GetCachedCheckFileResult(builder, fileName, sourceText, options) - - match cachedResults with - | Some (_, checkResults) -> return FSharpCheckFileAnswer.Succeeded checkResults - | None -> - if beingCheckedFileTable.TryAdd(beingCheckedFileKey, ()) then - let hash: SourceTextHash = sourceText.GetHashCode() |> int64 - try - // Get additional script #load closure information if applicable. - // For scripts, this will have been recorded by GetProjectOptionsFromScript. - let tcConfig = tcPrior.TcConfig - let loadClosure = scriptClosureCache.TryGet(AnyCallerThread, options) - let! checkAnswer = - FSharpCheckFileResults.CheckOneFile - (parseResults, - sourceText, - fileName, - options.ProjectFileName, - tcConfig, - tcPrior.TcGlobals, - tcPrior.TcImports, - tcInfo.tcState, - tcInfo.moduleNamesDict, - loadClosure, - tcInfo.TcErrors, - options.IsIncompleteTypeCheckEnvironment, - options, - builder, - Array.ofList tcInfo.tcDependencyFiles, - creationDiags, - parseResults.Diagnostics, - keepAssemblyContents, - suggestNamesForErrors) |> Cancellable.toAsync - let parsingOptions = FSharpParsingOptions.FromTcConfig(tcConfig, Array.ofList builder.SourceFiles, options.UseScriptResolutionRules) - reactor.SetPreferredUILang tcConfig.preferredUiLang - bc.RecordCheckFileInProjectResults(fileName, options, parsingOptions, parseResults, fileVersion, tcPrior.TimeStamp, Some checkAnswer, hash) - return FSharpCheckFileAnswer.Succeeded checkAnswer - finally - let dummy = ref () - beingCheckedFileTable.TryRemove(beingCheckedFileKey, dummy) |> ignore - else - do! Async.Sleep 100 - if stopwatch.Elapsed > TimeSpan.FromMinutes 1. then - return FSharpCheckFileAnswer.Aborted - else - return! loop() - } - return! loop() - } + creationDiags: FSharpDiagnostic[]) = + + node { + match! bc.GetCachedCheckFileResult(builder, fileName, sourceText, options) with + | Some (_, results) -> return FSharpCheckFileAnswer.Succeeded results + | _ -> + let lazyCheckFile = + getCheckFileNode + (parseResults, sourceText, fileName, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) + (fun () -> + Interlocked.Increment(&actualCheckFileCount) |> ignore + ) + + let! (_, results, _, _) = lazyCheckFile.GetOrComputeValue() + return FSharpCheckFileAnswer.Succeeded results + } /// Type-check the result obtained by parsing, but only if the antecedent type checking context is available. member bc.CheckFileInProjectAllowingStaleCachedResults(parseResults: FSharpParseFileResults, filename, fileVersion, sourceText: ISourceText, options, userOpName) = - async { - try - if implicitlyStartBackgroundWork then - reactor.CancelBackgroundOp() // cancel the background work, since we will start new work after we're done - - let! cachedResults = - async { - let! builderOpt, creationDiags = getAnyBuilder reactor (options, userOpName, "CheckFileInProjectAllowingStaleCachedResults ", filename) - - match builderOpt with - | Some builder -> - match bc.GetCachedCheckFileResult(builder, filename, sourceText, options) with - | Some (_, checkResults) -> return Some (builder, creationDiags, Some (FSharpCheckFileAnswer.Succeeded checkResults)) - | _ -> return Some (builder, creationDiags, None) - | _ -> return None // the builder wasn't ready - } + node { + let! cachedResults = + node { + let! builderOpt, creationDiags = getAnyBuilder (options, userOpName) + + match builderOpt with + | Some builder -> + match! bc.GetCachedCheckFileResult(builder, filename, sourceText, options) with + | Some (_, checkResults) -> return Some (builder, creationDiags, Some (FSharpCheckFileAnswer.Succeeded checkResults)) + | _ -> return Some (builder, creationDiags, None) + | _ -> return None // the builder wasn't ready + } - match cachedResults with - | None -> return None - | Some (_, _, Some x) -> return Some x - | Some (builder, creationDiags, None) -> - Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CheckFileInProjectAllowingStaleCachedResults.CacheMiss", filename) - let tcPrior = - let tcPrior = builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename - tcPrior - |> Option.bind (fun tcPrior -> - match tcPrior.TryTcInfo with - | Some(tcInfo) -> Some (tcPrior, tcInfo) - | _ -> None - ) + match cachedResults with + | None -> return None + | Some (_, _, Some x) -> return Some x + | Some (builder, creationDiags, None) -> + Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CheckFileInProjectAllowingStaleCachedResults.CacheMiss", filename) + let tcPrior = + let tcPrior = builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename + tcPrior + |> Option.bind (fun tcPrior -> + match tcPrior.TryTcInfo with + | Some(tcInfo) -> Some (tcPrior, tcInfo) + | _ -> None + ) - match tcPrior with - | Some(tcPrior, tcInfo) -> - let! checkResults = bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) - return Some checkResults - | None -> return None // the incremental builder was not up to date - finally - bc.ImplicitlyStartCheckProjectInBackground(options, userOpName) + match tcPrior with + | Some(tcPrior, tcInfo) -> + let! checkResults = bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) + return Some checkResults + | None -> return None // the incremental builder was not up to date } /// Type-check the result obtained by parsing. Force the evaluation of the antecedent type checking context if needed. member bc.CheckFileInProject(parseResults: FSharpParseFileResults, filename, fileVersion, sourceText: ISourceText, options, userOpName) = - let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "CheckFileInProject", filename, action) + node { + let! builderOpt,creationDiags = getOrCreateBuilder (options, userOpName) + match builderOpt with + | None -> return FSharpCheckFileAnswer.Succeeded (FSharpCheckFileResults.MakeEmpty(filename, creationDiags, keepAssemblyContents)) + | Some builder -> + // Check the cache. We can only use cached results when there is no work to do to bring the background builder up-to-date + let! cachedResults = bc.GetCachedCheckFileResult(builder, filename, sourceText, options) - async { - try - if implicitlyStartBackgroundWork then - reactor.CancelBackgroundOp() // cancel the background work, since we will start new work after we're done - let! builderOpt,creationDiags = getBuilder reactor (options, userOpName, "CheckFileInProject", filename) - match builderOpt with - | None -> return FSharpCheckFileAnswer.Succeeded (FSharpCheckFileResults.MakeEmpty(filename, creationDiags, keepAssemblyContents)) - | Some builder -> - // Check the cache. We can only use cached results when there is no work to do to bring the background builder up-to-date - let cachedResults = bc.GetCachedCheckFileResult(builder, filename, sourceText, options) - - match cachedResults with - | Some (_, checkResults) -> return FSharpCheckFileAnswer.Succeeded checkResults - | _ -> - // In order to prevent blocking of the reactor thread of getting a prior file, we try to get the results if it is considered up-to-date. - // If it's not up-to-date, then use the reactor thread to evaluate and get the results. - let! tcPrior, tcInfo = - match builder.TryGetCheckResultsBeforeFileInProject filename with - | Some(tcPrior) when tcPrior.TryTcInfo.IsSome -> - async { return (tcPrior, tcPrior.TryTcInfo.Value) } - | _ -> - execWithReactorAsync <| fun ctok -> - cancellable { - let! tcPrior = builder.GetCheckResultsBeforeFileInProject (ctok, filename) - let! tcInfo = tcPrior.GetTcInfo() |> Eventually.toCancellable - return (tcPrior, tcInfo) - } - let! checkAnswer = bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) - return checkAnswer - finally - bc.ImplicitlyStartCheckProjectInBackground(options, userOpName) + match cachedResults with + | Some (_, checkResults) -> return FSharpCheckFileAnswer.Succeeded checkResults + | _ -> + let! tcPrior, tcInfo = + match builder.TryGetCheckResultsBeforeFileInProject filename with + | Some(tcPrior) when tcPrior.TryTcInfo.IsSome -> + node { return (tcPrior, tcPrior.TryTcInfo.Value) } + | _ -> + node { + let! tcPrior = builder.GetCheckResultsBeforeFileInProject (filename) + let! tcInfo = tcPrior.GetTcInfo() + return (tcPrior, tcInfo) + } + return! bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) } /// Parses and checks the source file and returns untyped AST and check results. member bc.ParseAndCheckFileInProject (filename:string, fileVersion, sourceText: ISourceText, options:FSharpProjectOptions, userOpName) = - let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "ParseAndCheckFileInProject", filename, action) + node { + let strGuid = "_ProjectId=" + (options.ProjectId |> Option.defaultValue "null") + Logger.LogBlockMessageStart (filename + strGuid) LogCompilerFunctionId.Service_ParseAndCheckFileInProject - async { - try - let strGuid = "_ProjectId=" + (options.ProjectId |> Option.defaultValue "null") - Logger.LogBlockMessageStart (filename + strGuid) LogCompilerFunctionId.Service_ParseAndCheckFileInProject - - if implicitlyStartBackgroundWork then - Logger.LogMessage (filename + strGuid + "-Cancelling background work") LogCompilerFunctionId.Service_ParseAndCheckFileInProject - reactor.CancelBackgroundOp() // cancel the background work, since we will start new work after we're done - - let! builderOpt,creationDiags = getBuilder reactor (options, userOpName, "ParseAndCheckFileInProject", filename) - match builderOpt with - | None -> - Logger.LogBlockMessageStop (filename + strGuid + "-Failed_Aborted") LogCompilerFunctionId.Service_ParseAndCheckFileInProject - - let parseTree = EmptyParsedInput(filename, (false, false)) - let parseResults = FSharpParseFileResults(creationDiags, parseTree, true, [| |]) - return (parseResults, FSharpCheckFileAnswer.Aborted) - - | Some builder -> - let cachedResults = bc.GetCachedCheckFileResult(builder, filename, sourceText, options) - - match cachedResults with - | Some (parseResults, checkResults) -> - Logger.LogBlockMessageStop (filename + strGuid + "-Successful_Cached") LogCompilerFunctionId.Service_ParseAndCheckFileInProject - - return parseResults, FSharpCheckFileAnswer.Succeeded checkResults - | _ -> - // In order to prevent blocking of the reactor thread of getting a prior file, we try to get the results if it is considered up-to-date. - // If it's not up-to-date, then use the reactor thread to evaluate and get the results. - let! tcPrior, tcInfo = - match builder.TryGetCheckResultsBeforeFileInProject filename with - | Some(tcPrior) when tcPrior.TryTcInfo.IsSome -> - async { return (tcPrior, tcPrior.TryTcInfo.Value) } - | _ -> - execWithReactorAsync <| fun ctok -> - cancellable { - let! tcPrior = builder.GetCheckResultsBeforeFileInProject (ctok, filename) - let! tcInfo = tcPrior.GetTcInfo() |> Eventually.toCancellable - return (tcPrior, tcInfo) - } + let! builderOpt,creationDiags = getOrCreateBuilder (options, userOpName) + match builderOpt with + | None -> + Logger.LogBlockMessageStop (filename + strGuid + "-Failed_Aborted") LogCompilerFunctionId.Service_ParseAndCheckFileInProject + + let parseTree = EmptyParsedInput(filename, (false, false)) + let parseResults = FSharpParseFileResults(creationDiags, parseTree, true, [| |]) + return (parseResults, FSharpCheckFileAnswer.Aborted) + + | Some builder -> + let! cachedResults = bc.GetCachedCheckFileResult(builder, filename, sourceText, options) + + match cachedResults with + | Some (parseResults, checkResults) -> + Logger.LogBlockMessageStop (filename + strGuid + "-Successful_Cached") LogCompilerFunctionId.Service_ParseAndCheckFileInProject + + return (parseResults, FSharpCheckFileAnswer.Succeeded checkResults) + | _ -> + let! tcPrior, tcInfo = + match builder.TryGetCheckResultsBeforeFileInProject filename with + | Some(tcPrior) when tcPrior.TryTcInfo.IsSome -> + node { return (tcPrior, tcPrior.TryTcInfo.Value) } + | _ -> + node { + let! tcPrior = builder.GetCheckResultsBeforeFileInProject (filename) + let! tcInfo = tcPrior.GetTcInfo() + return (tcPrior, tcInfo) + } - // Do the parsing. - let parsingOptions = FSharpParsingOptions.FromTcConfig(builder.TcConfig, Array.ofList (builder.SourceFiles), options.UseScriptResolutionRules) - reactor.SetPreferredUILang tcPrior.TcConfig.preferredUiLang - let parseDiags, parseTree, anyErrors = ParseAndCheckFile.parseFile (sourceText, filename, parsingOptions, userOpName, suggestNamesForErrors) - let parseResults = FSharpParseFileResults(parseDiags, parseTree, anyErrors, builder.AllDependenciesDeprecated) - let! checkResults = bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) - - Logger.LogBlockMessageStop (filename + strGuid + "-Successful") LogCompilerFunctionId.Service_ParseAndCheckFileInProject - - return parseResults, checkResults - finally - bc.ImplicitlyStartCheckProjectInBackground(options, userOpName) + // Do the parsing. + let parsingOptions = FSharpParsingOptions.FromTcConfig(builder.TcConfig, Array.ofList (builder.SourceFiles), options.UseScriptResolutionRules) + GraphNode.SetPreferredUILang tcPrior.TcConfig.preferredUiLang + let parseDiags, parseTree, anyErrors = ParseAndCheckFile.parseFile (sourceText, filename, parsingOptions, userOpName, suggestNamesForErrors) + let parseResults = FSharpParseFileResults(parseDiags, parseTree, anyErrors, builder.AllDependenciesDeprecated) + let! checkResults = bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior, tcInfo, creationDiags) + + Logger.LogBlockMessageStop (filename + strGuid + "-Successful") LogCompilerFunctionId.Service_ParseAndCheckFileInProject + + return (parseResults, checkResults) } /// Fetch the check information from the background compiler (which checks w.r.t. the FileSystem API) member _.GetBackgroundCheckResultsForFileInProject(filename, options, userOpName) = - reactor.EnqueueAndAwaitOpAsync(userOpName, "GetBackgroundCheckResultsForFileInProject", filename, fun ctok -> - cancellable { - let! builderOpt, creationDiags = getOrCreateBuilder (ctok, options, userOpName) + node { + let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with | None -> let parseTree = EmptyParsedInput(filename, (false, false)) @@ -731,9 +716,9 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC return (parseResults, typedResults) | Some builder -> let (parseTree, _, _, parseDiags) = builder.GetParseResultsForFile (filename) - let! tcProj = builder.GetFullCheckResultsAfterFileInProject (ctok, filename) + let! tcProj = builder.GetFullCheckResultsAfterFileInProject (filename) - let! tcInfo, tcInfoExtras = tcProj.GetTcInfoWithExtras() |> Eventually.toCancellable + let! tcInfo, tcInfoExtras = tcProj.GetTcInfoWithExtras() let tcResolutionsRev = tcInfoExtras.tcResolutionsRev let tcSymbolUsesRev = tcInfoExtras.tcSymbolUsesRev @@ -775,112 +760,120 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC List.head tcOpenDeclarationsRev) return (parseResults, typedResults) } - ) member _.FindReferencesInFile(filename: string, options: FSharpProjectOptions, symbol: FSharpSymbol, canInvalidateProject: bool, userOpName: string) = - reactor.EnqueueAndAwaitOpAsync(userOpName, "FindReferencesInFile", filename, fun ctok -> - cancellable { - let! builderOpt, _ = getOrCreateBuilderWithInvalidationFlag (ctok, options, canInvalidateProject, userOpName) - match builderOpt with - | None -> return Seq.empty - | Some builder -> - if builder.ContainsFile filename then - let! checkResults = builder.GetFullCheckResultsAfterFileInProject (ctok, filename) - let! keyStoreOpt = checkResults.TryGetItemKeyStore() |> Eventually.toCancellable - match keyStoreOpt with - | None -> return Seq.empty - | Some reader -> return reader.FindAll symbol.Item - else - return Seq.empty - }) + node { + let! builderOpt, _ = getOrCreateBuilderWithInvalidationFlag (options, canInvalidateProject, userOpName) + match builderOpt with + | None -> return Seq.empty + | Some builder -> + if builder.ContainsFile filename then + let! checkResults = builder.GetFullCheckResultsAfterFileInProject (filename) + let! keyStoreOpt = checkResults.TryGetItemKeyStore() + match keyStoreOpt with + | None -> return Seq.empty + | Some reader -> return reader.FindAll symbol.Item + else + return Seq.empty + } member _.GetSemanticClassificationForFile(filename: string, options: FSharpProjectOptions, userOpName: string) = - reactor.EnqueueAndAwaitOpAsync(userOpName, "GetSemanticClassificationForFile", filename, fun ctok -> - cancellable { - let! builderOpt, _ = getOrCreateBuilder (ctok, options, userOpName) - match builderOpt with + node { + let! builderOpt, _ = getOrCreateBuilder (options, userOpName) + match builderOpt with + | None -> return None + | Some builder -> + let! checkResults = builder.GetFullCheckResultsAfterFileInProject (filename) + let! scopt = checkResults.GetSemanticClassification() + match scopt with | None -> return None - | Some builder -> - let! checkResults = builder.GetFullCheckResultsAfterFileInProject (ctok, filename) - let! scopt = checkResults.GetSemanticClassification() |> Eventually.toCancellable - match scopt with - | None -> return None - | Some sc -> return Some (sc.GetView ()) }) + | Some sc -> return Some (sc.GetView ()) + } /// Try to get recent approximate type check results for a file. member _.TryGetRecentCheckResultsForFile(filename: string, options:FSharpProjectOptions, sourceText: ISourceText option, _userOpName: string) = - parseCacheLock.AcquireLock (fun ltok -> - match sourceText with - | Some sourceText -> - let hash = sourceText.GetHashCode() |> int64 - match checkFileInProjectCache.TryGet(ltok,(filename,hash,options)) with - | Some (a,b,c,_) -> Some (a,b,c) - | None -> checkFileInProjectCachePossiblyStale.TryGet(ltok,(filename,options)) - | None -> checkFileInProjectCachePossiblyStale.TryGet(ltok,(filename,options))) + match sourceText with + | Some sourceText -> + let hash = sourceText.GetHashCode() |> int64 + let resOpt = parseCacheLock.AcquireLock(fun ltok -> checkFileInProjectCache.TryGet(ltok,(filename,hash,options))) + match resOpt with + | Some res -> + match res.TryPeekValue() with + | ValueSome(a,b,c,_) -> + Some(a,b,c) + | ValueNone -> + None + | None -> + None + | None -> + None /// Parse and typecheck the whole project (the implementation, called recursively as project graph is evaluated) - member private _.ParseAndCheckProjectImpl(options, ctok, userOpName) = - cancellable { - let! builderOpt,creationDiags = getOrCreateBuilder (ctok, options, userOpName) - match builderOpt with - | None -> - return FSharpCheckProjectResults (options.ProjectFileName, None, keepAssemblyContents, creationDiags, None) - | Some builder -> - let! (tcProj, ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt) = builder.GetFullCheckResultsAndImplementationsForProject(ctok) - let errorOptions = tcProj.TcConfig.errorSeverityOptions - let fileName = TcGlobals.DummyFileNameForRangesWithoutASpecificLocation - - let! tcInfo, tcInfoExtras = tcProj.GetTcInfoWithExtras() |> Eventually.toCancellable - - let tcSymbolUses = tcInfoExtras.TcSymbolUses - let topAttribs = tcInfo.topAttribs - let tcState = tcInfo.tcState - let tcEnvAtEnd = tcInfo.tcEnvAtEndOfFile - let tcErrors = tcInfo.TcErrors - let tcDependencyFiles = tcInfo.tcDependencyFiles - let diagnostics = - [| yield! creationDiags; - yield! DiagnosticHelpers.CreateDiagnostics (errorOptions, true, fileName, tcErrors, suggestNamesForErrors) |] - let results = - FSharpCheckProjectResults - (options.ProjectFileName, - Some tcProj.TcConfig, - keepAssemblyContents, - diagnostics, - Some(tcProj.TcGlobals, tcProj.TcImports, tcState.Ccu, tcState.CcuSig, - tcSymbolUses, topAttribs, tcAssemblyDataOpt, ilAssemRef, - tcEnvAtEnd.AccessRights, tcAssemblyExprOpt, - Array.ofList tcDependencyFiles, - options)) - return results + member private _.ParseAndCheckProjectImpl(options, userOpName) = + node { + let! builderOpt,creationDiags = getOrCreateBuilder (options, userOpName) + match builderOpt with + | None -> + return FSharpCheckProjectResults (options.ProjectFileName, None, keepAssemblyContents, creationDiags, None) + | Some builder -> + let! (tcProj, ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt) = builder.GetFullCheckResultsAndImplementationsForProject() + let errorOptions = tcProj.TcConfig.errorSeverityOptions + let fileName = TcGlobals.DummyFileNameForRangesWithoutASpecificLocation + + let! tcInfo, tcInfoExtras = tcProj.GetTcInfoWithExtras() + + let tcSymbolUses = tcInfoExtras.TcSymbolUses + let topAttribs = tcInfo.topAttribs + let tcState = tcInfo.tcState + let tcEnvAtEnd = tcInfo.tcEnvAtEndOfFile + let tcErrors = tcInfo.TcErrors + let tcDependencyFiles = tcInfo.tcDependencyFiles + let diagnostics = + [| yield! creationDiags; + yield! DiagnosticHelpers.CreateDiagnostics (errorOptions, true, fileName, tcErrors, suggestNamesForErrors) |] + let results = + FSharpCheckProjectResults + (options.ProjectFileName, + Some tcProj.TcConfig, + keepAssemblyContents, + diagnostics, + Some(tcProj.TcGlobals, tcProj.TcImports, tcState.Ccu, tcState.CcuSig, + tcSymbolUses, topAttribs, tcAssemblyDataOpt, ilAssemRef, + tcEnvAtEnd.AccessRights, tcAssemblyExprOpt, + Array.ofList tcDependencyFiles, + options)) + return results } - member _.GetAssemblyData(options, ctok, userOpName) = - cancellable { - let! builderOpt,_ = getOrCreateBuilder (ctok, options, userOpName) + member _.GetAssemblyData(options, userOpName) = + node { + let! builderOpt,_ = getOrCreateBuilder (options, userOpName) match builderOpt with | None -> return None | Some builder -> - let! (_, _, tcAssemblyDataOpt, _) = builder.GetCheckResultsAndImplementationsForProject(ctok) + let! (_, _, tcAssemblyDataOpt, _) = builder.GetCheckResultsAndImplementationsForProject() return tcAssemblyDataOpt } /// Get the timestamp that would be on the output if fully built immediately member private _.TryGetLogicalTimeStampForProject(cache, options) = - match tryGetBuilder options with - | Some (Some builder, _) -> Some (builder.GetLogicalTimeStampForProject(cache)) - | _ -> None - + match tryGetBuilderNode options with + | Some lazyWork -> + match lazyWork.TryPeekValue() with + | ValueSome (Some builder, _) -> + Some(builder.GetLogicalTimeStampForProject(cache)) + | _ -> + None + | _ -> + None /// Parse and typecheck the whole project. member bc.ParseAndCheckProject(options, userOpName) = - reactor.EnqueueAndAwaitOpAsync(userOpName, "ParseAndCheckProject", options.ProjectFileName, fun ctok -> bc.ParseAndCheckProjectImpl(options, ctok, userOpName)) - - member _.GetProjectOptionsFromScript(filename, sourceText, previewEnabled, loadedTimeStamp, otherFlags, useFsiAuxLib: bool option, useSdkRefs: bool option, sdkDirOverride: string option, assumeDotNetFramework: bool option, optionsStamp: int64 option, userOpName) = + bc.ParseAndCheckProjectImpl(options, userOpName) - reactor.EnqueueAndAwaitOpAsync (userOpName, "GetProjectOptionsFromScript", filename, fun ctok -> + member _.GetProjectOptionsFromScript(filename, sourceText, previewEnabled, loadedTimeStamp, otherFlags, useFsiAuxLib: bool option, useSdkRefs: bool option, sdkDirOverride: string option, assumeDotNetFramework: bool option, optionsStamp: int64 option, _userOpName) = cancellable { use errors = new ErrorScope() @@ -909,8 +902,8 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC let fsiCompilerOptions = CompilerOptions.GetCoreFsiCompilerOptions tcConfigB CompilerOptions.ParseCompilerOptions (ignore, fsiCompilerOptions, Array.toList otherFlags) - let loadClosure = - LoadClosure.ComputeClosureOfScriptText(ctok, legacyReferenceResolver, + let loadClosure = + LoadClosure.ComputeClosureOfScriptText(legacyReferenceResolver, FSharpCheckerResultsSettings.defaultFSharpBinariesDir, filename, sourceText, CodeContext.Editing, useSimpleResolution, useFsiAuxLib, useSdkRefs, sdkDirOverride, new Lexhelp.LexResourceManager(), applyCompilerOptions, assumeDotNetFramework, @@ -940,68 +933,30 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC scriptClosureCache.Set(AnyCallerThread, options, loadClosure) // Save the full load closure for later correlation. let diags = loadClosure.LoadClosureRootFileDiagnostics |> List.map (fun (exn, isError) -> FSharpDiagnostic.CreateFromException(exn, isError, range.Zero, false)) return options, (diags @ errors.Diagnostics) - }) + } + |> Cancellable.toAsync - member bc.InvalidateConfiguration(options : FSharpProjectOptions, startBackgroundCompileIfAlreadySeen, userOpName) = - let startBackgroundCompileIfAlreadySeen = defaultArg startBackgroundCompileIfAlreadySeen implicitlyStartBackgroundWork - - // This operation can't currently be cancelled nor awaited - reactor.EnqueueOp(userOpName, "InvalidateConfiguration: Stamp(" + (options.Stamp |> Option.defaultValue 0L).ToString() + ")", options.ProjectFileName, fun ctok -> - // If there was a similar entry then re-establish an empty builder . This is a somewhat arbitrary choice - it - // will have the effect of releasing memory associated with the previous builder, but costs some time. - if incrementalBuildersCache.ContainsSimilarKey (AnyCallerThread, options) then - - // We do not need to decrement here - it is done by disposal. - let newBuilderInfo = CreateOneIncrementalBuilder (ctok, options, userOpName) |> Cancellable.runWithoutCancellation - incrementalBuildersCache.Set(AnyCallerThread, options, newBuilderInfo) - - // Start working on the project. Also a somewhat arbitrary choice - if startBackgroundCompileIfAlreadySeen then - bc.CheckProjectInBackground(options, userOpName + ".StartBackgroundCompile")) + member bc.InvalidateConfiguration(options: FSharpProjectOptions, userOpName) = + if incrementalBuildersCache.ContainsSimilarKey (AnyCallerThread, options) then + let _ = createBuilderNode (options, userOpName, CancellationToken.None) + () - member bc.ClearCache(options : FSharpProjectOptions seq, userOpName) = - // This operation can't currently be cancelled nor awaited - reactor.EnqueueOp(userOpName, "ClearCache", String.Empty, fun _ -> + member bc.ClearCache(options: seq, _userOpName) = + lock gate (fun () -> options - |> Seq.iter (fun options -> incrementalBuildersCache.RemoveAnySimilar(AnyCallerThread, options))) - - member _.NotifyProjectCleaned (options : FSharpProjectOptions, userOpName) = - reactor.EnqueueAndAwaitOpAsync(userOpName, "NotifyProjectCleaned", options.ProjectFileName, fun ctok -> - cancellable { - // If there was a similar entry (as there normally will have been) then re-establish an empty builder . This - // is a somewhat arbitrary choice - it will have the effect of releasing memory associated with the previous - // builder, but costs some time. - if incrementalBuildersCache.ContainsSimilarKey (AnyCallerThread, options) then - // We do not need to decrement here - it is done by disposal. - let! newBuilderInfo = CreateOneIncrementalBuilder (ctok, options, userOpName) - incrementalBuildersCache.Set(AnyCallerThread, options, newBuilderInfo) - }) - - member _.CheckProjectInBackground (options, userOpName) = - reactor.SetBackgroundOp - (Some(userOpName, "CheckProjectInBackground", options.ProjectFileName, - (fun ctok -> - eventually { - // Builder creation is not yet time-sliced. - let! builderOpt,_ = getOrCreateBuilder (ctok, options, userOpName) |> Eventually.ofCancellable - match builderOpt with - | None -> return () - | Some builder -> - return! builder.PopulatePartialCheckingResults (ctok) - }))) - - member _.StopBackgroundCompile () = - reactor.SetBackgroundOp(None) - - member _.WaitForBackgroundCompile() = - reactor.WaitForBackgroundOpCompletion() - - member _.CompleteAllQueuedOps() = - reactor.CompleteAllQueuedOps() - - member _.Reactor = reactor - - member _.ReactorOps = reactorOps + |> Seq.iter (fun options -> incrementalBuildersCache.RemoveAnySimilar(AnyCallerThread, options)) + ) + + member _.NotifyProjectCleaned (options: FSharpProjectOptions, userOpName) = + async { + let! ct = Async.CancellationToken + // If there was a similar entry (as there normally will have been) then re-establish an empty builder . This + // is a somewhat arbitrary choice - it will have the effect of releasing memory associated with the previous + // builder, but costs some time. + if incrementalBuildersCache.ContainsSimilarKey (AnyCallerThread, options) then + let _ = createBuilderNode (options, userOpName, ct) + () + } member _.BeforeBackgroundFileCheck = beforeFileChecked.Publish @@ -1011,34 +966,34 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC member _.ProjectChecked = projectChecked.Publish - member _.CurrentQueueLength = reactor.CurrentQueueLength - - member _.ClearCachesAsync (userOpName) = - reactor.EnqueueAndAwaitOpAsync (userOpName, "ClearCachesAsync", "", fun ctok -> - parseCacheLock.AcquireLock (fun ltok -> - checkFileInProjectCachePossiblyStale.Clear ltok - checkFileInProjectCache.Clear ltok - parseFileCache.Clear(ltok)) - incrementalBuildersCache.Clear(AnyCallerThread) - frameworkTcImportsCache.Clear ctok - scriptClosureCache.Clear (AnyCallerThread) - cancellable.Return ()) - - member _.DownsizeCaches(userOpName) = - reactor.EnqueueAndAwaitOpAsync (userOpName, "DownsizeCaches", "", fun ctok -> - parseCacheLock.AcquireLock (fun ltok -> - checkFileInProjectCachePossiblyStale.Resize(ltok, newKeepStrongly=1) - checkFileInProjectCache.Resize(ltok, newKeepStrongly=1) - parseFileCache.Resize(ltok, newKeepStrongly=1)) - incrementalBuildersCache.Resize(AnyCallerThread, newKeepStrongly=1, newKeepMax=1) - frameworkTcImportsCache.Downsize(ctok) - scriptClosureCache.Resize(AnyCallerThread,newKeepStrongly=1, newKeepMax=1) - cancellable.Return ()) + member _.ClearCachesAsync (_userOpName) = + async { + return + lock gate (fun () -> + parseCacheLock.AcquireLock (fun ltok -> + checkFileInProjectCache.Clear(ltok) + parseFileCache.Clear(ltok)) + incrementalBuildersCache.Clear(AnyCallerThread) + frameworkTcImportsCache.Clear() + scriptClosureCache.Clear (AnyCallerThread) + ) + } + + member _.DownsizeCaches(_userOpName) = + async { + return + lock gate (fun () -> + parseCacheLock.AcquireLock (fun ltok -> + checkFileInProjectCache.Resize(ltok, newKeepStrongly=1) + parseFileCache.Resize(ltok, newKeepStrongly=1)) + incrementalBuildersCache.Resize(AnyCallerThread, newKeepStrongly=1, newKeepMax=1) + frameworkTcImportsCache.Downsize() + scriptClosureCache.Resize(AnyCallerThread,newKeepStrongly=1, newKeepMax=1) + ) + } member _.FrameworkImportsCache = frameworkTcImportsCache - member _.ImplicitlyStartBackgroundWork with get() = implicitlyStartBackgroundWork and set v = implicitlyStartBackgroundWork <- v - static member ActualParseFileCount = actualParseFileCount static member ActualCheckFileCount = actualCheckFileCount @@ -1057,7 +1012,8 @@ type FSharpChecker(legacyReferenceResolver, enablePartialTypeChecking) = let backgroundCompiler = - BackgroundCompiler(legacyReferenceResolver, + BackgroundCompiler( + legacyReferenceResolver, projectCacheSize, keepAssemblyContents, keepAllBackgroundResolutions, @@ -1082,7 +1038,16 @@ type FSharpChecker(legacyReferenceResolver, let maxMemEvent = new Event() /// Instantiate an interactive checker. - static member Create(?projectCacheSize, ?keepAssemblyContents, ?keepAllBackgroundResolutions, ?legacyReferenceResolver, ?tryGetMetadataSnapshot, ?suggestNamesForErrors, ?keepAllBackgroundSymbolUses, ?enableBackgroundItemKeyStoreAndSemanticClassification, ?enablePartialTypeChecking) = + static member Create( + ?projectCacheSize, + ?keepAssemblyContents, + ?keepAllBackgroundResolutions, + ?legacyReferenceResolver, + ?tryGetMetadataSnapshot, + ?suggestNamesForErrors, + ?keepAllBackgroundSymbolUses, + ?enableBackgroundItemKeyStoreAndSemanticClassification, + ?enablePartialTypeChecking) = let legacyReferenceResolver = match legacyReferenceResolver with @@ -1148,10 +1113,12 @@ type FSharpChecker(legacyReferenceResolver, member _.GetBackgroundParseResultsForFileInProject (filename,options, ?userOpName: string) = let userOpName = defaultArg userOpName "Unknown" backgroundCompiler.GetBackgroundParseResultsForFileInProject(filename, options, userOpName) + |> Async.AwaitNodeCode member _.GetBackgroundCheckResultsForFileInProject (filename,options, ?userOpName: string) = let userOpName = defaultArg userOpName "Unknown" backgroundCompiler.GetBackgroundCheckResultsForFileInProject(filename,options, userOpName) + |> Async.AwaitNodeCode /// Try to get recent approximate type check results for a file. member _.TryGetRecentCheckResultsForFile(filename: string, options:FSharpProjectOptions, ?sourceText, ?userOpName: string) = @@ -1159,25 +1126,24 @@ type FSharpChecker(legacyReferenceResolver, backgroundCompiler.TryGetRecentCheckResultsForFile(filename,options,sourceText,userOpName) member _.Compile(argv: string[], ?userOpName: string) = - let userOpName = defaultArg userOpName "Unknown" - backgroundCompiler.Reactor.EnqueueAndAwaitOpAsync (userOpName, "Compile", "", fun ctok -> - cancellable { - return CompileHelpers.compileFromArgs (ctok, argv, legacyReferenceResolver, None, None) - }) + let _userOpName = defaultArg userOpName "Unknown" + async { + let ctok = CompilationThreadToken() + return CompileHelpers.compileFromArgs (ctok, argv, legacyReferenceResolver, None, None) + } member _.Compile (ast:ParsedInput list, assemblyName:string, outFile:string, dependencies:string list, ?pdbFile:string, ?executable:bool, ?noframework:bool, ?userOpName: string) = - let userOpName = defaultArg userOpName "Unknown" - backgroundCompiler.Reactor.EnqueueAndAwaitOpAsync (userOpName, "Compile", assemblyName, fun ctok -> - cancellable { - let noframework = defaultArg noframework false - return CompileHelpers.compileFromAsts (ctok, legacyReferenceResolver, ast, assemblyName, outFile, dependencies, noframework, pdbFile, executable, None, None) - } - ) + let _userOpName = defaultArg userOpName "Unknown" + async { + let ctok = CompilationThreadToken() + let noframework = defaultArg noframework false + return CompileHelpers.compileFromAsts (ctok, legacyReferenceResolver, ast, assemblyName, outFile, dependencies, noframework, pdbFile, executable, None, None) + } member _.CompileToDynamicAssembly (otherFlags: string[], execute: (TextWriter * TextWriter) option, ?userOpName: string) = - let userOpName = defaultArg userOpName "Unknown" - backgroundCompiler.Reactor.EnqueueAndAwaitOpAsync (userOpName, "CompileToDynamicAssembly", "", fun ctok -> - cancellable { + let _userOpName = defaultArg userOpName "Unknown" + async { + let ctok = CompilationThreadToken() CompileHelpers.setOutputStreams execute // References used to capture the results of compilation @@ -1187,7 +1153,7 @@ type FSharpChecker(legacyReferenceResolver, // Function to generate and store the results of compilation let debugInfo = otherFlags |> Array.exists (fun arg -> arg = "-g" || arg = "--debug:+" || arg = "/debug:+") - let dynamicAssemblyCreator = Some (CompileHelpers.createDynamicAssembly (ctok, debugInfo, tcImportsRef, execute.IsSome, assemblyBuilderRef)) + let dynamicAssemblyCreator = Some (CompileHelpers.createDynamicAssembly (debugInfo, tcImportsRef, execute.IsSome, assemblyBuilderRef)) // Perform the compilation, given the above capturing function. let errorsAndWarnings, result = CompileHelpers.compileFromArgs (ctok, otherFlags, legacyReferenceResolver, tcImportsCapture, dynamicAssemblyCreator) @@ -1199,13 +1165,12 @@ type FSharpChecker(legacyReferenceResolver, | Some a -> Some (a :> System.Reflection.Assembly) return errorsAndWarnings, result, assemblyOpt - } - ) + } member _.CompileToDynamicAssembly (ast:ParsedInput list, assemblyName:string, dependencies:string list, execute: (TextWriter * TextWriter) option, ?debug:bool, ?noframework:bool, ?userOpName: string) = - let userOpName = defaultArg userOpName "Unknown" - backgroundCompiler.Reactor.EnqueueAndAwaitOpAsync (userOpName, "CompileToDynamicAssembly", assemblyName, fun ctok -> - cancellable { + let _userOpName = defaultArg userOpName "Unknown" + async { + let ctok = CompilationThreadToken() CompileHelpers.setOutputStreams execute // References used to capture the results of compilation @@ -1221,7 +1186,7 @@ type FSharpChecker(legacyReferenceResolver, let outFile = Path.Combine(location, assemblyName + ".dll") // Function to generate and store the results of compilation - let dynamicAssemblyCreator = Some (CompileHelpers.createDynamicAssembly (ctok, debugInfo, tcImportsRef, execute.IsSome, assemblyBuilderRef)) + let dynamicAssemblyCreator = Some (CompileHelpers.createDynamicAssembly (debugInfo, tcImportsRef, execute.IsSome, assemblyBuilderRef)) // Perform the compilation, given the above capturing function. let errorsAndWarnings, result = @@ -1234,8 +1199,7 @@ type FSharpChecker(legacyReferenceResolver, | Some a -> Some (a :> System.Reflection.Assembly) return errorsAndWarnings, result, assemblyOpt - } - ) + } /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. /// For example, the type provider approvals file may have changed. @@ -1257,7 +1221,6 @@ type FSharpChecker(legacyReferenceResolver, // If the maxMB limit is reached, drastic action is taken // - reduce strong cache sizes to a minimum let userOpName = "MaxMemoryReached" - backgroundCompiler.CompleteAllQueuedOps() maxMemoryReached <- true braceMatchCache.Resize(AnyCallerThread, newKeepStrongly=10) backgroundCompiler.DownsizeCaches(userOpName) |> Async.RunSynchronously @@ -1265,18 +1228,16 @@ type FSharpChecker(legacyReferenceResolver, // This is for unit testing only member ic.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() = - backgroundCompiler.CompleteAllQueuedOps() // flush AsyncOp ic.ClearCachesAsync() |> Async.RunSynchronously System.GC.Collect() System.GC.WaitForPendingFinalizers() - backgroundCompiler.CompleteAllQueuedOps() // flush AsyncOp FxResolver.ClearStaticCaches() /// This function is called when the configuration is known to have changed for reasons not encoded in the ProjectOptions. /// For example, dependent references may have been deleted or created. - member _.InvalidateConfiguration(options: FSharpProjectOptions, ?startBackgroundCompile, ?userOpName: string) = + member _.InvalidateConfiguration(options: FSharpProjectOptions, ?userOpName: string) = let userOpName = defaultArg userOpName "Unknown" - backgroundCompiler.InvalidateConfiguration(options, startBackgroundCompile, userOpName) + backgroundCompiler.InvalidateConfiguration(options, userOpName) /// Clear the internal cache of the given projects. member _.ClearCache(options: FSharpProjectOptions seq, ?userOpName: string) = @@ -1293,6 +1254,7 @@ type FSharpChecker(legacyReferenceResolver, member _.CheckFileInProjectAllowingStaleCachedResults(parseResults:FSharpParseFileResults, filename:string, fileVersion:int, source:string, options:FSharpProjectOptions, ?userOpName: string) = let userOpName = defaultArg userOpName "Unknown" backgroundCompiler.CheckFileInProjectAllowingStaleCachedResults(parseResults,filename,fileVersion,SourceText.ofString source,options,userOpName) + |> Async.AwaitNodeCode /// Typecheck a source code file, returning a handle to the results of the /// parse including the reconstructed types in the file. @@ -1300,6 +1262,7 @@ type FSharpChecker(legacyReferenceResolver, let userOpName = defaultArg userOpName "Unknown" ic.CheckMaxMemoryReached() backgroundCompiler.CheckFileInProject(parseResults,filename,fileVersion,sourceText,options,userOpName) + |> Async.AwaitNodeCode /// Typecheck a source code file, returning a handle to the results of the /// parse including the reconstructed types in the file. @@ -1307,22 +1270,26 @@ type FSharpChecker(legacyReferenceResolver, let userOpName = defaultArg userOpName "Unknown" ic.CheckMaxMemoryReached() backgroundCompiler.ParseAndCheckFileInProject(filename, fileVersion, sourceText, options, userOpName) + |> Async.AwaitNodeCode member ic.ParseAndCheckProject(options, ?userOpName: string) = let userOpName = defaultArg userOpName "Unknown" ic.CheckMaxMemoryReached() backgroundCompiler.ParseAndCheckProject(options, userOpName) + |> Async.AwaitNodeCode member ic.FindBackgroundReferencesInFile(filename:string, options: FSharpProjectOptions, symbol: FSharpSymbol, ?canInvalidateProject: bool, ?userOpName: string) = let canInvalidateProject = defaultArg canInvalidateProject true let userOpName = defaultArg userOpName "Unknown" ic.CheckMaxMemoryReached() backgroundCompiler.FindReferencesInFile(filename, options, symbol, canInvalidateProject, userOpName) + |> Async.AwaitNodeCode member ic.GetBackgroundSemanticClassificationForFile(filename:string, options: FSharpProjectOptions, ?userOpName) = let userOpName = defaultArg userOpName "Unknown" ic.CheckMaxMemoryReached() backgroundCompiler.GetSemanticClassificationForFile(filename, options, userOpName) + |> Async.AwaitNodeCode /// For a given script file, get the ProjectOptions implied by the #load closure member _.GetProjectOptionsFromScript(filename, source, ?previewEnabled, ?loadedTimeStamp, ?otherFlags, ?useFsiAuxLib, ?useSdkRefs, ?assumeDotNetFramework, ?sdkDirOverride, ?optionsStamp: int64, ?userOpName: string) = @@ -1380,29 +1347,6 @@ type FSharpChecker(legacyReferenceResolver, member ic.GetParsingOptionsFromCommandLineArgs(argv, ?isInteractive: bool, ?isEditing) = ic.GetParsingOptionsFromCommandLineArgs([], argv, ?isInteractive=isInteractive, ?isEditing=isEditing) - /// Begin background parsing the given project. - member _.StartBackgroundCompile(options, ?userOpName) = - let userOpName = defaultArg userOpName "Unknown" - backgroundCompiler.CheckProjectInBackground(options, userOpName) - - /// Begin background parsing the given project. - member ic.CheckProjectInBackground(options, ?userOpName) = - ic.StartBackgroundCompile(options, ?userOpName=userOpName) - - /// Stop the background compile. - member _.StopBackgroundCompile() = - backgroundCompiler.StopBackgroundCompile() - - /// Block until the background compile finishes. - // - // This is for unit testing only - member _.WaitForBackgroundCompile() = backgroundCompiler.WaitForBackgroundCompile() - - // Publish the ReactorOps from the background compiler for internal use - member ic.ReactorOps = backgroundCompiler.ReactorOps - - member _.CurrentQueueLength = backgroundCompiler.CurrentQueueLength - member _.BeforeBackgroundFileCheck = backgroundCompiler.BeforeBackgroundFileCheck member _.FileParsed = backgroundCompiler.FileParsed @@ -1411,10 +1355,6 @@ type FSharpChecker(legacyReferenceResolver, member _.ProjectChecked = backgroundCompiler.ProjectChecked - member _.ImplicitlyStartBackgroundWork with get() = backgroundCompiler.ImplicitlyStartBackgroundWork and set v = backgroundCompiler.ImplicitlyStartBackgroundWork <- v - - member _.PauseBeforeBackgroundWork with get() = Reactor.Singleton.PauseBeforeBackgroundWork and set v = Reactor.Singleton.PauseBeforeBackgroundWork <- v - static member ActualParseFileCount = BackgroundCompiler.ActualParseFileCount static member ActualCheckFileCount = BackgroundCompiler.ActualCheckFileCount diff --git a/src/fsharp/service/service.fsi b/src/fsharp/service/service.fsi index 0e5d5c9218..97b6f9f798 100644 --- a/src/fsharp/service/service.fsi +++ b/src/fsharp/service/service.fsi @@ -349,7 +349,7 @@ type public FSharpChecker = /// The options for the project or script, used to determine active --define conditionals and other options relevant to parsing. /// Optionally, specify source that must match the previous parse precisely. /// An optional string used for tracing compiler operations associated with this request. - member TryGetRecentCheckResultsForFile: filename: string * options:FSharpProjectOptions * ?sourceText: ISourceText * ?userOpName: string -> (FSharpParseFileResults * FSharpCheckFileResults * (*version*)int) option + member TryGetRecentCheckResultsForFile: filename: string * options:FSharpProjectOptions * ?sourceText: ISourceText * ?userOpName: string -> (FSharpParseFileResults * FSharpCheckFileResults * (* hash *)int64) option /// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation. member InvalidateAll: unit -> unit @@ -359,26 +359,14 @@ type public FSharpChecker = /// For example, dependent references may have been deleted or created. /// /// The options for the project or script, used to determine active --define conditionals and other options relevant to parsing. - /// Start a background compile of the project if a project with the same name has already been seen before. /// An optional string used for tracing compiler operations associated with this request. - member InvalidateConfiguration: options: FSharpProjectOptions * ?startBackgroundCompile: bool * ?userOpName: string -> unit + member InvalidateConfiguration: options: FSharpProjectOptions * ?userOpName: string -> unit /// Clear the internal cache of the given projects. /// The given project options. /// An optional string used for tracing compiler operations associated with this request. member ClearCache: options: FSharpProjectOptions seq * ?userOpName: string -> unit - /// Set the project to be checked in the background. Overrides any previous call to CheckProjectInBackground. - member CheckProjectInBackground: options: FSharpProjectOptions * ?userOpName: string -> unit - - /// Stop the background compile. - //[] - member StopBackgroundCompile: unit -> unit - - /// Block until the background compile finishes. - //[] - member WaitForBackgroundCompile: unit -> unit - /// Report a statistic for testability static member ActualParseFileCount: int @@ -388,11 +376,6 @@ type public FSharpChecker = /// Flush all caches and garbage collect member ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients: unit -> unit - /// Current queue length of the service, for debug purposes. - /// In addition, a single async operation or a step of a background build - /// may be in progress - such an operation is not counted in the queue length. - member CurrentQueueLength: int - /// /// This function is called when a project has been cleaned/rebuilt, and thus any live type providers should be refreshed. /// @@ -427,26 +410,11 @@ type public FSharpChecker = /// member MaxMemory: int with get, set - /// - /// Get or set a flag which controls if background work is started implicitly. - /// - /// If true, calls to CheckFileInProject implicitly start a background check of that project, replacing - /// any other background checks in progress. This is useful in IDE applications with spare CPU cycles as - /// it prepares the project analysis results for use. The default is 'true'. - /// - member ImplicitlyStartBackgroundWork: bool with get, set - - /// Get or set the pause time in milliseconds before background work is started. - member PauseBeforeBackgroundWork: int with get, set - /// Notify the host that a project has been fully checked in the background (using file contents provided by the file system API) /// /// The event may be raised on a background thread. member ProjectChecked: IEvent - // For internal use only - member internal ReactorOps: IReactorOperations - [] static member Instance: FSharpChecker member internal FrameworkImportsCache: FrameworkImportsCache diff --git a/src/fsharp/symbols/SymbolHelpers.fs b/src/fsharp/symbols/SymbolHelpers.fs index 17ec5893c3..13f8300464 100644 --- a/src/fsharp/symbols/SymbolHelpers.fs +++ b/src/fsharp/symbols/SymbolHelpers.fs @@ -177,19 +177,6 @@ type internal CompilationErrorLogger (debugName: string, options: FSharpDiagnost member x.GetDiagnostics() = diagnostics.ToArray() - -/// This represents the thread-local state established as each task function runs as part of the build. -/// -/// Use to reset error and warning handlers. -type CompilationGlobalsScope(errorLogger: ErrorLogger, phase: BuildPhase) = - let unwindEL = PushErrorLoggerPhaseUntilUnwind(fun _ -> errorLogger) - let unwindBP = PushThreadBuildPhaseUntilUnwind phase - // Return the disposable object that cleans up - interface IDisposable with - member d.Dispose() = - unwindBP.Dispose() - unwindEL.Dispose() - module DiagnosticHelpers = let ReportDiagnostic (options: FSharpDiagnosticOptions, allErrors, mainInputFileName, fileInfo, (exn, severity), suggestNames) = diff --git a/src/fsharp/symbols/SymbolHelpers.fsi b/src/fsharp/symbols/SymbolHelpers.fsi index d46e096e2a..760798ba03 100755 --- a/src/fsharp/symbols/SymbolHelpers.fsi +++ b/src/fsharp/symbols/SymbolHelpers.fsi @@ -92,13 +92,6 @@ namespace FSharp.Compiler.Diagnostics /// Get the captured diagnostics member GetDiagnostics: unit -> (PhasedDiagnostic * FSharpDiagnosticSeverity)[] - /// This represents the global state established as each task function runs as part of the build. - /// - /// Use to reset error and warning handlers. - type internal CompilationGlobalsScope = - new : ErrorLogger * BuildPhase -> CompilationGlobalsScope - interface IDisposable - module internal DiagnosticHelpers = val ReportDiagnostic: FSharpDiagnosticOptions * allErrors: bool * mainInputFileName: string * fileInfo: (int * int) * (PhasedDiagnostic * FSharpDiagnosticSeverity) * suggestNames: bool -> FSharpDiagnostic list diff --git a/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs b/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs index cfff214df5..c5a1e2e1e6 100644 --- a/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs +++ b/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs @@ -1973,8 +1973,6 @@ FSharp.Compiler.CodeAnalysis.FSharpCheckProjectResults: System.String ToString() FSharp.Compiler.CodeAnalysis.FSharpCheckProjectResults: System.String[] DependencyFiles FSharp.Compiler.CodeAnalysis.FSharpCheckProjectResults: System.String[] get_DependencyFiles() FSharp.Compiler.CodeAnalysis.FSharpChecker -FSharp.Compiler.CodeAnalysis.FSharpChecker: Boolean ImplicitlyStartBackgroundWork -FSharp.Compiler.CodeAnalysis.FSharpChecker: Boolean get_ImplicitlyStartBackgroundWork() FSharp.Compiler.CodeAnalysis.FSharpChecker: FSharp.Compiler.CodeAnalysis.FSharpChecker Create(Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.String,System.DateTime],Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`3[System.Object,System.IntPtr,System.Int32]]]], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean]) FSharp.Compiler.CodeAnalysis.FSharpChecker: FSharp.Compiler.CodeAnalysis.FSharpChecker Instance FSharp.Compiler.CodeAnalysis.FSharpChecker: FSharp.Compiler.CodeAnalysis.FSharpChecker get_Instance() @@ -1982,14 +1980,10 @@ FSharp.Compiler.CodeAnalysis.FSharpChecker: FSharp.Compiler.CodeAnalysis.FSharpP FSharp.Compiler.CodeAnalysis.FSharpChecker: FSharp.Compiler.Tokenization.FSharpTokenInfo[][] TokenizeFile(System.String) FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 ActualCheckFileCount FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 ActualParseFileCount -FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 CurrentQueueLength FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 MaxMemory -FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 PauseBeforeBackgroundWork FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_ActualCheckFileCount() FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_ActualParseFileCount() -FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_CurrentQueueLength() FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_MaxMemory() -FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_PauseBeforeBackgroundWork() FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileAnswer] CheckFileInProject(FSharp.Compiler.CodeAnalysis.FSharpParseFileResults, System.String, Int32, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpCheckProjectResults] ParseAndCheckProject(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults] GetBackgroundParseResultsForFileInProject(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String]) @@ -2018,21 +2012,16 @@ FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Mi FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Microsoft.FSharp.Control.FSharpHandler`1[System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]],System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]] get_BeforeBackgroundFileCheck() FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Microsoft.FSharp.Control.FSharpHandler`1[System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]],System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]] get_FileChecked() FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Microsoft.FSharp.Control.FSharpHandler`1[System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]],System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]] get_FileParsed() -FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`3[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults,System.Int32]] TryGetRecentCheckResultsForFile(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText], Microsoft.FSharp.Core.FSharpOption`1[System.String]) +FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`3[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults,System.Int64]] TryGetRecentCheckResultsForFile(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText], Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParsingOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]] GetParsingOptionsFromCommandLineArgs(Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean]) FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParsingOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]] GetParsingOptionsFromCommandLineArgs(Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean]) FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParsingOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]] GetParsingOptionsFromProjectOptions(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions) FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.Tokenization.FSharpTokenInfo[],FSharp.Compiler.Tokenization.FSharpTokenizerLexState] TokenizeLine(System.String, FSharp.Compiler.Tokenization.FSharpTokenizerLexState) -FSharp.Compiler.CodeAnalysis.FSharpChecker: Void CheckProjectInBackground(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpChecker: Void ClearCache(System.Collections.Generic.IEnumerable`1[FSharp.Compiler.CodeAnalysis.FSharpProjectOptions], Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpChecker: Void ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() FSharp.Compiler.CodeAnalysis.FSharpChecker: Void InvalidateAll() -FSharp.Compiler.CodeAnalysis.FSharpChecker: Void InvalidateConfiguration(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.String]) -FSharp.Compiler.CodeAnalysis.FSharpChecker: Void StopBackgroundCompile() -FSharp.Compiler.CodeAnalysis.FSharpChecker: Void WaitForBackgroundCompile() -FSharp.Compiler.CodeAnalysis.FSharpChecker: Void set_ImplicitlyStartBackgroundWork(Boolean) +FSharp.Compiler.CodeAnalysis.FSharpChecker: Void InvalidateConfiguration(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpChecker: Void set_MaxMemory(Int32) -FSharp.Compiler.CodeAnalysis.FSharpChecker: Void set_PauseBeforeBackgroundWork(Int32) FSharp.Compiler.CodeAnalysis.FSharpParseFileResults FSharp.Compiler.CodeAnalysis.FSharpParseFileResults: Boolean IsBindingALambdaAtPosition(FSharp.Compiler.Text.Position) FSharp.Compiler.CodeAnalysis.FSharpParseFileResults: Boolean IsPosContainedInApplication(FSharp.Compiler.Text.Position) diff --git a/tests/FSharp.Compiler.UnitTests/BuildGraphTests.fs b/tests/FSharp.Compiler.UnitTests/BuildGraphTests.fs new file mode 100644 index 0000000000..e46d4e88f4 --- /dev/null +++ b/tests/FSharp.Compiler.UnitTests/BuildGraphTests.fs @@ -0,0 +1,273 @@ +// 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.Threading +open System.Runtime.CompilerServices +open Xunit +open FSharp.Test.Utilities +open FSharp.Compiler.BuildGraph +open Internal.Utilities.Library + +module BuildGraphTests = + + [] + let private createNode () = + let o = obj () + GraphNode(node { + Assert.shouldBeTrue (o <> null) + return 1 + }), WeakReference(o) + + [] + let ``Intialization of graph node should not have a computed value``() = + let node = GraphNode(node { return 1 }) + Assert.shouldBeTrue(node.TryPeekValue().IsNone) + Assert.shouldBeFalse(node.HasValue) + + [] + let ``Two requests to get a value asynchronously should be successful``() = + let resetEvent = new ManualResetEvent(false) + let resetEventInAsync = new ManualResetEvent(false) + + let graphNode = + GraphNode(node { + resetEventInAsync.Set() |> ignore + let! _ = NodeCode.AwaitWaitHandle_ForTesting(resetEvent) + return 1 + }) + + let task1 = + node { + let! _ = graphNode.GetOrComputeValue() + () + } |> NodeCode.StartAsTask_ForTesting + + let task2 = + node { + let! _ = graphNode.GetOrComputeValue() + () + } |> NodeCode.StartAsTask_ForTesting + + resetEventInAsync.WaitOne() |> ignore + resetEvent.Set() |> ignore + try + task1.Wait(1000) |> ignore + task2.Wait() |> ignore + with + | :? TimeoutException -> reraise() + | _ -> () + + [] + let ``Many requests to get a value asynchronously should only evaluate the computation once``() = + let requests = 10000 + let mutable computationCount = 0 + + let graphNode = + GraphNode(node { + computationCount <- computationCount + 1 + return 1 + }) + + let work = Async.Parallel(Array.init requests (fun _ -> graphNode.GetOrComputeValue() |> Async.AwaitNodeCode)) + + Async.RunSynchronously(work) + |> ignore + + Assert.shouldBe 1 computationCount + + [] + let ``Many requests to get a value asynchronously should get the correct value``() = + let requests = 10000 + + let graphNode = GraphNode(node { return 1 }) + + let work = Async.Parallel(Array.init requests (fun _ -> graphNode.GetOrComputeValue() |> Async.AwaitNodeCode)) + + let result = Async.RunSynchronously(work) + + Assert.shouldNotBeEmpty result + Assert.shouldBe requests result.Length + result + |> Seq.iter (Assert.shouldBe 1) + + [] + let ``A request to get a value asynchronously should have its computation cleaned up by the GC``() = + let graphNode, weak = createNode () + + GC.Collect(2, GCCollectionMode.Forced, true) + + Assert.shouldBeTrue weak.IsAlive + + NodeCode.RunImmediateWithoutCancellation(graphNode.GetOrComputeValue()) + |> ignore + + GC.Collect(2, GCCollectionMode.Forced, true) + + Assert.shouldBeFalse weak.IsAlive + + [] + let ``Many requests to get a value asynchronously should have its computation cleaned up by the GC``() = + let requests = 10000 + + let graphNode, weak = createNode () + + GC.Collect(2, GCCollectionMode.Forced, true) + + Assert.shouldBeTrue weak.IsAlive + + Async.RunSynchronously(Async.Parallel(Array.init requests (fun _ -> graphNode.GetOrComputeValue() |> Async.AwaitNodeCode))) + |> ignore + + GC.Collect(2, GCCollectionMode.Forced, true) + + Assert.shouldBeFalse weak.IsAlive + + [] + let ``A request can cancel``() = + let graphNode = + GraphNode(node { + return 1 + }) + + use cts = new CancellationTokenSource() + + let work = + node { + cts.Cancel() + return! graphNode.GetOrComputeValue() + } + + let ex = + try + NodeCode.RunImmediate(work, ct = cts.Token) + |> ignore + failwith "Should have canceled" + with + | :? OperationCanceledException as ex -> + ex + + Assert.shouldBeTrue(ex <> null) + + [] + let ``A request can cancel 2``() = + let resetEvent = new ManualResetEvent(false) + + let graphNode = + GraphNode(node { + let! _ = NodeCode.AwaitWaitHandle_ForTesting(resetEvent) + return 1 + }) + + use cts = new CancellationTokenSource() + + let task = + node { + cts.Cancel() + resetEvent.Set() |> ignore + } + |> NodeCode.StartAsTask_ForTesting + + let ex = + try + NodeCode.RunImmediate(graphNode.GetOrComputeValue(), ct = cts.Token) + |> ignore + failwith "Should have canceled" + with + | :? OperationCanceledException as ex -> + ex + + Assert.shouldBeTrue(ex <> null) + try task.Wait(1000) |> ignore with | :? TimeoutException -> reraise() | _ -> () + + [] + let ``Many requests to get a value asynchronously might evaluate the computation more than once even when some requests get canceled``() = + let requests = 10000 + let resetEvent = new ManualResetEvent(false) + let mutable computationCountBeforeSleep = 0 + let mutable computationCount = 0 + + let graphNode = + GraphNode(node { + computationCountBeforeSleep <- computationCountBeforeSleep + 1 + let! _ = NodeCode.AwaitWaitHandle_ForTesting(resetEvent) + computationCount <- computationCount + 1 + return 1 + }) + + use cts = new CancellationTokenSource() + + let work = + node { + let! _ = graphNode.GetOrComputeValue() + () + } + + let tasks = ResizeArray() + + for i = 0 to requests - 1 do + if i % 10 = 0 then + NodeCode.StartAsTask_ForTesting(work, ct = cts.Token) + |> tasks.Add + else + NodeCode.StartAsTask_ForTesting(work) + |> tasks.Add + + cts.Cancel() + resetEvent.Set() |> ignore + NodeCode.RunImmediateWithoutCancellation(work) + |> ignore + + Assert.shouldBeTrue cts.IsCancellationRequested + Assert.shouldBeTrue(computationCountBeforeSleep > 0) + Assert.shouldBeTrue(computationCount >= 0) + + tasks + |> Seq.iter (fun x -> + try x.Wait(1000) |> ignore with | :? TimeoutException -> reraise() | _ -> ()) + + [] + let ``No-RetryCompute - Many requests to get a value asynchronously should only evaluate the computation once even when some requests get canceled``() = + let requests = 10000 + let resetEvent = new ManualResetEvent(false) + let mutable computationCountBeforeSleep = 0 + let mutable computationCount = 0 + + let graphNode = + GraphNode(false, node { + computationCountBeforeSleep <- computationCountBeforeSleep + 1 + let! _ = NodeCode.AwaitWaitHandle_ForTesting(resetEvent) + computationCount <- computationCount + 1 + return 1 + }) + + use cts = new CancellationTokenSource() + + let work = + node { + let! _ = graphNode.GetOrComputeValue() + () + } + + let tasks = ResizeArray() + + for i = 0 to requests - 1 do + if i % 10 = 0 then + NodeCode.StartAsTask_ForTesting(work, ct = cts.Token) + |> tasks.Add + else + NodeCode.StartAsTask_ForTesting(work) + |> tasks.Add + + cts.Cancel() + resetEvent.Set() |> ignore + NodeCode.RunImmediateWithoutCancellation(work) + |> ignore + + Assert.shouldBeTrue cts.IsCancellationRequested + Assert.shouldBe 1 computationCountBeforeSleep + Assert.shouldBe 1 computationCount + + tasks + |> Seq.iter (fun x -> + try x.Wait(1000) |> ignore with | :? TimeoutException -> reraise() | _ -> ()) diff --git a/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj b/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj index faf5a1d8b2..c3d3ca35ae 100644 --- a/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj +++ b/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj @@ -24,6 +24,7 @@ + diff --git a/tests/FSharp.Test.Utilities/CompilerAssert.fs b/tests/FSharp.Test.Utilities/CompilerAssert.fs index c2f4e33607..c9e0d50582 100644 --- a/tests/FSharp.Test.Utilities/CompilerAssert.fs +++ b/tests/FSharp.Test.Utilities/CompilerAssert.fs @@ -191,10 +191,8 @@ type CompilerAssert private () = checkEqual "ErrorRange" expectedErrorRange actualErrorRange checkEqual "Message" expectedErrorMsg actualErrorMsg) - static let gate = obj () - static let compile isExe options source f = - lock gate (fun _ -> compileAux isExe options source f) + compileAux isExe options source f static let rec compileCompilationAux outputPath (disposals: ResizeArray) ignoreWarnings (cmpl: Compilation) : (FSharpDiagnostic[] * string) * string list = let compilationRefs, deps = @@ -357,15 +355,14 @@ type CompilerAssert private () = static member CompileWithErrors(cmpl: Compilation, expectedErrors, ?ignoreWarnings) = let ignoreWarnings = defaultArg ignoreWarnings false - lock gate (fun () -> - compileCompilation ignoreWarnings cmpl (fun ((errors, _), _) -> - assertErrors 0 ignoreWarnings errors expectedErrors)) + compileCompilation ignoreWarnings cmpl (fun ((errors, _), _) -> + assertErrors 0 ignoreWarnings errors expectedErrors) static member Compile(cmpl: Compilation, ?ignoreWarnings) = CompilerAssert.CompileWithErrors(cmpl, [||], defaultArg ignoreWarnings false) static member CompileRaw(cmpl: Compilation, ?ignoreWarnings) = - lock gate (fun () -> returnCompilation cmpl (defaultArg ignoreWarnings false)) + returnCompilation cmpl (defaultArg ignoreWarnings false) static member ExecuteAndReturnResult (outputFilePath: string, deps: string list, newProcess: bool) = // If we execute in-process (true by default), then the only way of getting STDOUT is to redirect it to SB, and STDERR is from catching an exception. @@ -379,17 +376,16 @@ type CompilerAssert private () = let beforeExecute = defaultArg beforeExecute (fun _ _ -> ()) let newProcess = defaultArg newProcess false let onOutput = defaultArg onOutput (fun _ -> ()) - lock gate (fun () -> - compileCompilation ignoreWarnings cmpl (fun ((errors, outputFilePath), deps) -> - assertErrors 0 ignoreWarnings errors [||] - beforeExecute outputFilePath deps - if newProcess then - let (exitCode, output, errors) = executeBuiltAppNewProcessAndReturnResult outputFilePath - if exitCode <> 0 then - Assert.Fail errors - onOutput output - else - executeBuiltApp outputFilePath deps)) + compileCompilation ignoreWarnings cmpl (fun ((errors, outputFilePath), deps) -> + assertErrors 0 ignoreWarnings errors [||] + beforeExecute outputFilePath deps + if newProcess then + let (exitCode, output, errors) = executeBuiltAppNewProcessAndReturnResult outputFilePath + if exitCode <> 0 then + Assert.Fail errors + onOutput output + else + executeBuiltApp outputFilePath deps) static member ExecutionHasOutput(cmpl: Compilation, expectedOutput: string) = CompilerAssert.Execute(cmpl, newProcess = true, onOutput = (fun output -> Assert.AreEqual(expectedOutput, output, sprintf "'%s' = '%s'" expectedOutput output))) @@ -446,118 +442,112 @@ type CompilerAssert private () = Option.get assembly static member Pass (source: string) = - lock gate <| fun () -> - let parseResults, fileAnswer = checker.ParseAndCheckFileInProject("test.fs", 0, SourceText.ofString source, defaultProjectOptions) |> Async.RunSynchronously + let parseResults, fileAnswer = checker.ParseAndCheckFileInProject("test.fs", 0, SourceText.ofString source, defaultProjectOptions) |> Async.RunSynchronously - Assert.IsEmpty(parseResults.Diagnostics, sprintf "Parse errors: %A" parseResults.Diagnostics) + Assert.IsEmpty(parseResults.Diagnostics, sprintf "Parse errors: %A" parseResults.Diagnostics) - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted") - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted") + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> - Assert.IsEmpty(typeCheckResults.Diagnostics, sprintf "Type Check errors: %A" typeCheckResults.Diagnostics) + Assert.IsEmpty(typeCheckResults.Diagnostics, sprintf "Type Check errors: %A" typeCheckResults.Diagnostics) static member PassWithOptions options (source: string) = - lock gate <| fun () -> - let options = { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions} + let options = { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions} - let parseResults, fileAnswer = checker.ParseAndCheckFileInProject("test.fs", 0, SourceText.ofString source, options) |> Async.RunSynchronously + let parseResults, fileAnswer = checker.ParseAndCheckFileInProject("test.fs", 0, SourceText.ofString source, options) |> Async.RunSynchronously - Assert.IsEmpty(parseResults.Diagnostics, sprintf "Parse errors: %A" parseResults.Diagnostics) + Assert.IsEmpty(parseResults.Diagnostics, sprintf "Parse errors: %A" parseResults.Diagnostics) - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted") - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted") + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> - Assert.IsEmpty(typeCheckResults.Diagnostics, sprintf "Type Check errors: %A" typeCheckResults.Diagnostics) + Assert.IsEmpty(typeCheckResults.Diagnostics, sprintf "Type Check errors: %A" typeCheckResults.Diagnostics) static member TypeCheckWithErrorsAndOptionsAgainstBaseLine options (sourceDirectory:string) (sourceFile: string) = - lock gate <| fun () -> - let absoluteSourceFile = System.IO.Path.Combine(sourceDirectory, sourceFile) - let parseResults, fileAnswer = - checker.ParseAndCheckFileInProject( - sourceFile, - 0, - SourceText.ofString (File.ReadAllText absoluteSourceFile), - { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions; SourceFiles = [|sourceFile|] }) - |> Async.RunSynchronously + let absoluteSourceFile = System.IO.Path.Combine(sourceDirectory, sourceFile) + let parseResults, fileAnswer = + checker.ParseAndCheckFileInProject( + sourceFile, + 0, + SourceText.ofString (File.ReadAllText absoluteSourceFile), + { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions; SourceFiles = [|sourceFile|] }) + |> Async.RunSynchronously - Assert.IsEmpty(parseResults.Diagnostics, sprintf "Parse errors: %A" parseResults.Diagnostics) + Assert.IsEmpty(parseResults.Diagnostics, sprintf "Parse errors: %A" parseResults.Diagnostics) - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted") - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted") + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> - let errorsExpectedBaseLine = - let bslFile = Path.ChangeExtension(absoluteSourceFile, "bsl") - if not (FileSystem.FileExistsShim bslFile) then - // new test likely initialized, create empty baseline file - File.WriteAllText(bslFile, "") - File.ReadAllText(Path.ChangeExtension(absoluteSourceFile, "bsl")) - let errorsActual = - typeCheckResults.Diagnostics - |> Array.map (sprintf "%A") - |> String.concat "\n" - File.WriteAllText(Path.ChangeExtension(absoluteSourceFile,"err"), errorsActual) + let errorsExpectedBaseLine = + let bslFile = Path.ChangeExtension(absoluteSourceFile, "bsl") + if not (FileSystem.FileExistsShim bslFile) then + // new test likely initialized, create empty baseline file + File.WriteAllText(bslFile, "") + File.ReadAllText(Path.ChangeExtension(absoluteSourceFile, "bsl")) + let errorsActual = + typeCheckResults.Diagnostics + |> Array.map (sprintf "%A") + |> String.concat "\n" + File.WriteAllText(Path.ChangeExtension(absoluteSourceFile,"err"), errorsActual) - Assert.AreEqual(errorsExpectedBaseLine.Replace("\r\n","\n"), errorsActual.Replace("\r\n","\n")) + Assert.AreEqual(errorsExpectedBaseLine.Replace("\r\n","\n"), errorsActual.Replace("\r\n","\n")) static member TypeCheckWithOptionsAndName options name (source: string) = - lock gate <| fun () -> - let errors = - let parseResults, fileAnswer = - checker.ParseAndCheckFileInProject( - name, - 0, - SourceText.ofString source, - { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions; SourceFiles = [|name|] }) - |> Async.RunSynchronously - - if parseResults.Diagnostics.Length > 0 then - parseResults.Diagnostics - else - - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); [| |] - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> typeCheckResults.Diagnostics + let errors = + let parseResults, fileAnswer = + checker.ParseAndCheckFileInProject( + name, + 0, + SourceText.ofString source, + { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions; SourceFiles = [|name|] }) + |> Async.RunSynchronously - errors + if parseResults.Diagnostics.Length > 0 then + parseResults.Diagnostics + else - static member TypeCheckWithOptions options (source: string) = - lock gate <| fun () -> - let errors = - let parseResults, fileAnswer = - checker.ParseAndCheckFileInProject( - "test.fs", - 0, - SourceText.ofString source, - { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions}) - |> Async.RunSynchronously - - if parseResults.Diagnostics.Length > 0 then - parseResults.Diagnostics - else - - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); [| |] - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> typeCheckResults.Diagnostics + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); [| |] + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> typeCheckResults.Diagnostics - errors + errors - /// Parses and type checks the given source. Fails if type checker is aborted. - static member ParseAndTypeCheck(options, name, source: string) = - lock gate <| fun () -> + static member TypeCheckWithOptions options (source: string) = + let errors = let parseResults, fileAnswer = checker.ParseAndCheckFileInProject( - name, + "test.fs", 0, SourceText.ofString source, { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions}) |> Async.RunSynchronously - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); failwith "Type Checker Aborted" - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> parseResults, typeCheckResults + if parseResults.Diagnostics.Length > 0 then + parseResults.Diagnostics + else + + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); [| |] + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> typeCheckResults.Diagnostics + + errors + + /// Parses and type checks the given source. Fails if type checker is aborted. + static member ParseAndTypeCheck(options, name, source: string) = + let parseResults, fileAnswer = + checker.ParseAndCheckFileInProject( + name, + 0, + SourceText.ofString source, + { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions}) + |> Async.RunSynchronously + + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); failwith "Type Checker Aborted" + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> parseResults, typeCheckResults /// Parses and type checks the given source. Fails if the type checker is aborted or the parser returns any diagnostics. static member TypeCheck(options, name, source: string) = @@ -568,25 +558,24 @@ type CompilerAssert private () = checkResults static member TypeCheckWithErrorsAndOptionsAndAdjust options libAdjust (source: string) expectedTypeErrors = - lock gate <| fun () -> - let errors = - let parseResults, fileAnswer = - checker.ParseAndCheckFileInProject( - "test.fs", - 0, - SourceText.ofString source, - { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions}) - |> Async.RunSynchronously + let errors = + let parseResults, fileAnswer = + checker.ParseAndCheckFileInProject( + "test.fs", + 0, + SourceText.ofString source, + { defaultProjectOptions with OtherOptions = Array.append options defaultProjectOptions.OtherOptions}) + |> Async.RunSynchronously - if parseResults.Diagnostics.Length > 0 then - parseResults.Diagnostics - else + if parseResults.Diagnostics.Length > 0 then + parseResults.Diagnostics + else - match fileAnswer with - | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); [| |] - | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> typeCheckResults.Diagnostics + match fileAnswer with + | FSharpCheckFileAnswer.Aborted _ -> Assert.Fail("Type Checker Aborted"); [| |] + | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> typeCheckResults.Diagnostics - assertErrors libAdjust false errors expectedTypeErrors + assertErrors libAdjust false errors expectedTypeErrors static member TypeCheckWithErrorsAndOptions options (source: string) expectedTypeErrors = @@ -665,15 +654,14 @@ type CompilerAssert private () = errorMessages static member RunScriptWithOptions options (source: string) (expectedErrorMessages: string list) = - lock gate <| fun () -> - let errorMessages = CompilerAssert.RunScriptWithOptionsAndReturnResult options source - if expectedErrorMessages.Length <> errorMessages.Count then - Assert.Fail(sprintf "Expected error messages: %A \n\n Actual error messages: %A" expectedErrorMessages errorMessages) - else - (expectedErrorMessages, errorMessages) - ||> Seq.iter2 (fun expectedErrorMessage errorMessage -> - Assert.AreEqual(expectedErrorMessage, errorMessage) - ) + let errorMessages = CompilerAssert.RunScriptWithOptionsAndReturnResult options source + if expectedErrorMessages.Length <> errorMessages.Count then + Assert.Fail(sprintf "Expected error messages: %A \n\n Actual error messages: %A" expectedErrorMessages errorMessages) + else + (expectedErrorMessages, errorMessages) + ||> Seq.iter2 (fun expectedErrorMessage errorMessage -> + Assert.AreEqual(expectedErrorMessage, errorMessage) + ) static member RunScript source expectedErrorMessages = CompilerAssert.RunScriptWithOptions [||] source expectedErrorMessages diff --git a/tests/FSharp.Test.Utilities/TestFramework.fs b/tests/FSharp.Test.Utilities/TestFramework.fs index d2a3e812c7..efe60a1a41 100644 --- a/tests/FSharp.Test.Utilities/TestFramework.fs +++ b/tests/FSharp.Test.Utilities/TestFramework.fs @@ -13,6 +13,8 @@ open FSharp.Compiler.IO [] module Commands = + let gate = obj() + // Execute the process pathToExe passing the arguments: arguments with the working directory: workingDir timeout after timeout milliseconds -1 = wait forever // returns exit code, stdio and stderr as string arrays let executeProcess pathToExe arguments workingDir timeout = @@ -64,8 +66,10 @@ module Commands = else workingDir - File.WriteAllLines(Path.Combine(workingDir', "StandardOutput.txt"), outputList) - File.WriteAllLines(Path.Combine(workingDir', "StandardError.txt"), errorsList) + lock gate (fun () -> + File.WriteAllLines(Path.Combine(workingDir', "StandardOutput.txt"), outputList) + File.WriteAllLines(Path.Combine(workingDir', "StandardError.txt"), errorsList) + ) #endif p.ExitCode, outputList.ToArray(), errorsList.ToArray() | None -> -1, Array.empty, Array.empty diff --git a/tests/fsharp/Compiler/Service/MultiProjectTests.fs b/tests/fsharp/Compiler/Service/MultiProjectTests.fs index 573927f8c7..6b4bedc74f 100644 --- a/tests/fsharp/Compiler/Service/MultiProjectTests.fs +++ b/tests/fsharp/Compiler/Service/MultiProjectTests.fs @@ -76,7 +76,7 @@ let test() = 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() diff --git a/tests/fsharp/NUnitHelpers.fs b/tests/fsharp/NUnitHelpers.fs index 6af423d9b7..b80b3e54bf 100644 --- a/tests/fsharp/NUnitHelpers.fs +++ b/tests/fsharp/NUnitHelpers.fs @@ -2,6 +2,9 @@ namespace NUnit.Framework module Assert = + [] + do() + let inline fail message = Assert.Fail message let inline failf fmt = Printf.kprintf fail fmt diff --git a/tests/fsharp/TypeProviderTests.fs b/tests/fsharp/TypeProviderTests.fs index b129bea904..971960ddcd 100644 --- a/tests/fsharp/TypeProviderTests.fs +++ b/tests/fsharp/TypeProviderTests.fs @@ -7,7 +7,7 @@ #load "../FSharp.Test.Utilities/TestFramework.fs" #load "single-test.fs" #else -[] +[] module FSharp.Test.FSharpSuite.TypeProviderTests #endif @@ -204,6 +204,7 @@ let helloWorldCSharp () = [] [] [] +[] let ``negative type provider tests`` (name:string) = let cfg = testConfig "typeProviders/negTests" let dir = cfg.Directory diff --git a/tests/fsharp/tests.fs b/tests/fsharp/tests.fs index edca7419f6..214fbc8e82 100644 --- a/tests/fsharp/tests.fs +++ b/tests/fsharp/tests.fs @@ -34,6 +34,7 @@ let singleTestBuildAndRun = getTestsDirectory >> singleTestBuildAndRun let singleTestBuildAndRunVersion = getTestsDirectory >> singleTestBuildAndRunVersion let testConfig = getTestsDirectory >> testConfig +[] module CoreTests = // These tests are enabled for .NET Framework and .NET Core [] @@ -1853,6 +1854,7 @@ module CoreTests = #endif +[] module VersionTests = [] let ``member-selfidentifier-version4.6``() = singleTestBuildAndRunVersion "core/members/self-identifier/version46" FSC_BUILDONLY "4.6" @@ -1879,6 +1881,7 @@ module VersionTests = let ``nameof-fsi``() = singleTestBuildAndRunVersion "core/nameof/preview" FSI_BASIC "preview" #if !NETCOREAPP +[] module ToolsTests = // This test is disabled in coreclr builds dependent on fixing : https://github.com/Microsoft/visualfsharp/issues/2600 @@ -1908,6 +1911,7 @@ module ToolsTests = [] let ``eval-FSI_BASIC`` () = singleTestBuildAndRun "tools/eval" FSI_BASIC +[] module RegressionTests = [] @@ -2026,6 +2030,7 @@ module RegressionTests = peverify cfg "test.exe" +[] module OptimizationTests = [] @@ -2159,6 +2164,7 @@ module OptimizationTests = log "%s" m #endif +[] module TypecheckTests = [] let ``full-rank-arrays`` () = @@ -2892,7 +2898,7 @@ module TypecheckTests = [] let ``type check neg_byref_23`` () = singleNegTest (testConfig "typecheck/sigs") "neg_byref_23" - +[] module FscTests = [] let ``should be raised if AssemblyInformationalVersion has invalid version`` () = @@ -2962,6 +2968,7 @@ open System.Runtime.InteropServices #endif #if NET472 +[] module ProductVersionTest = let informationalVersionAttrName = typeof.FullName @@ -3030,6 +3037,7 @@ module GeneratedSignatureTests = #endif #if !NETCOREAPP +[] module OverloadResolution = module ``fsharpqa migrated tests`` = let [] ``Conformance\Expressions\SyntacticSugar (E_Slices01.fs)`` () = singleNegTest (testConfig "conformance/expressions/syntacticsugar") "E_Slices01" diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs index ffdc7f3c2a..05664377fd 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs @@ -46,22 +46,35 @@ type internal FSharpAnalysisSaveFileCommandHandler let document = solution.GetDocument(documentId) async { try - if document.Project.Language = LanguageNames.FSharp && not document.IsFSharpScript then + if document.Project.Language = LanguageNames.FSharp then let openDocIds = workspace.GetOpenDocumentIds() - let depProjIds = document.Project.GetDependentProjectIds().Add(document.Project.Id) let docIdsToReanalyze = - openDocIds - |> Seq.filter (fun x -> - depProjIds.Contains(x.ProjectId) && x <> document.Id && - ( - let doc = solution.GetDocument(x) - match box doc with - | null -> false - | _ -> doc.Project.Language = LanguageNames.FSharp + if document.IsFSharpScript then + openDocIds + |> Seq.filter (fun x -> + x <> document.Id && + ( + let doc = solution.GetDocument(x) + match doc with + | null -> false + | _ -> doc.IsFSharpScript + ) ) - ) - |> Array.ofSeq + |> Array.ofSeq + else + let depProjIds = document.Project.GetDependentProjectIds().Add(document.Project.Id) + openDocIds + |> Seq.filter (fun x -> + depProjIds.Contains(x.ProjectId) && x <> document.Id && + ( + let doc = solution.GetDocument(x) + match box doc with + | null -> false + | _ -> doc.Project.Language = LanguageNames.FSharp + ) + ) + |> Array.ofSeq if docIdsToReanalyze.Length > 0 then analyzerService.Reanalyze(workspace, documentIds=docIdsToReanalyze) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs index dff92ae1a8..44e2b4b968 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs @@ -44,7 +44,7 @@ type FSharpChecker with let parseAndCheckFile = async { - let! parseResults, checkFileAnswer = checker.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToFSharpSourceText(), options, userOpName=userOpName) + let! (parseResults, checkFileAnswer) = checker.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToFSharpSourceText(), options, userOpName=userOpName) return match checkFileAnswer with | FSharpCheckFileAnswer.Aborted -> @@ -55,7 +55,7 @@ type FSharpChecker with let tryGetFreshResultsWithTimeout() = async { - let! worker = Async.StartChild(parseAndCheckFile, millisecondsTimeout=languageServicePerformanceOptions.TimeUntilStaleCompletion) + let! worker = Async.StartChild(async { try return! parseAndCheckFile with | _ -> return None }, millisecondsTimeout=languageServicePerformanceOptions.TimeUntilStaleCompletion) try return! worker with :? TimeoutException -> diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerProvider.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerProvider.fs index 5413dffc85..ae2f2ca3bf 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerProvider.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerProvider.fs @@ -22,7 +22,6 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics type internal FSharpCheckerProvider [] ( - analyzerService: IFSharpDiagnosticAnalyzerService, [)>] workspace: VisualStudioWorkspace, projectContextFactory: IWorkspaceProjectContextFactory, settings: EditorOptions @@ -66,27 +65,6 @@ type internal FSharpCheckerProvider keepAllBackgroundSymbolUses = false, enableBackgroundItemKeyStoreAndSemanticClassification = true, enablePartialTypeChecking = true) - - // This is one half of the bridge between the F# background builder and the Roslyn analysis engine. - // When the F# background builder refreshes the background semantic build context for a file, - // we request Roslyn to reanalyze that individual file. - checker.BeforeBackgroundFileCheck.Add(fun (fileName, _extraProjectInfo) -> - // Only do this for scripts as misc script Rolsyn projects do not understand the dependencies of the script. e.x "#r "../test.dll"", "#load "test2.fsx"" - if isScriptFile fileName then - async { - try - let solution = workspace.CurrentSolution - let documentIds = solution.GetDocumentIdsWithFilePath(fileName) - if not documentIds.IsEmpty then - let documentIdsFiltered = documentIds |> Seq.filter workspace.IsDocumentOpen |> Seq.toArray - for documentId in documentIdsFiltered do - Trace.TraceInformation("{0:n3} Requesting Roslyn reanalysis of {1}", DateTime.Now.TimeOfDay.TotalSeconds, documentId) - if documentIdsFiltered.Length > 0 then - analyzerService.Reanalyze(workspace,documentIds=documentIdsFiltered) - with ex -> - Assert.Exception(ex) - } |> Async.StartImmediate - ) checker member this.Checker = checker.Value diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 5fafb1a115..aa5d35b081 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -57,8 +57,15 @@ module private FSharpProjectOptionsHelpers = oldProject.Version <> newProject.Version let hasDependentVersionChanged (oldProject: Project) (newProject: Project) (ct: CancellationToken) = + let oldProjectMetadataRefs = oldProject.MetadataReferences + let newProjectMetadataRefs = newProject.MetadataReferences + + if oldProjectMetadataRefs.Count <> newProjectMetadataRefs.Count then true + else + let oldProjectRefs = oldProject.ProjectReferences let newProjectRefs = newProject.ProjectReferences + oldProjectRefs.Count() <> newProjectRefs.Count() || (oldProjectRefs, newProjectRefs) ||> Seq.exists2 (fun p1 p2 -> @@ -68,7 +75,6 @@ module private FSharpProjectOptionsHelpers = let p2 = newProject.Solution.GetProject(p2.ProjectId) 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 @@ -157,7 +163,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor weakPEReferences.Add(comp, fsRefProj) fsRefProj - let rec tryComputeOptionsByFile (document: Document) (ct: CancellationToken) userOpName = + let rec tryComputeOptionsBySingleScriptOrFile (document: Document) (ct: CancellationToken) userOpName = async { let! fileStamp = document.GetTextVersionAsync(ct) |> Async.AwaitTask match singleFileCache.TryGetValue(document.Id) with @@ -203,8 +209,6 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor Stamp = Some(int64 (fileStamp.GetHashCode())) } - checkerProvider.Checker.CheckProjectInBackground(projectOptions, userOpName="checkOptions") - let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) singleFileCache.[document.Id] <- (fileStamp, parsingOptions, projectOptions) @@ -214,7 +218,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor | true, (fileStamp2, parsingOptions, projectOptions) -> if fileStamp <> fileStamp2 then singleFileCache.TryRemove(document.Id) |> ignore - return! tryComputeOptionsByFile document ct userOpName + return! tryComputeOptionsBySingleScriptOrFile document ct userOpName else return Some(parsingOptions, projectOptions) } @@ -277,6 +281,8 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor ) ) + let! ver = project.GetDependentVersionAsync(ct) |> Async.AwaitTask + let projectOptions = { ProjectFileName = projectSite.ProjectFileName @@ -289,7 +295,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor LoadTime = projectSite.LoadTime UnresolvedReferences = None OriginalLoadReferences = [] - Stamp = Some(int64 (project.Version.GetHashCode())) + Stamp = Some(int64 (ver.GetHashCode())) } // This can happen if we didn't receive the callback from HandleCommandLineChanges yet. @@ -318,7 +324,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor lastSuccessfulCompilations.TryRemove(pair.Key) |> ignore ) - checkerProvider.Checker.InvalidateConfiguration(projectOptions, startBackgroundCompile = false, userOpName = "computeOptions") + checkerProvider.Checker.InvalidateConfiguration(projectOptions, userOpName = "tryComputeOptions") let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) @@ -347,7 +353,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor if document.Project.Solution.Workspace.Kind = WorkspaceKind.MiscellaneousFiles then reply.Reply None elif document.Project.IsFSharpMiscellaneousOrMetadata then - let! options = tryComputeOptionsByFile document ct userOpName + let! options = tryComputeOptionsBySingleScriptOrFile document ct userOpName if ct.IsCancellationRequested then reply.Reply None else diff --git a/vsintegration/src/FSharp.LanguageService/BackgroundRequests.fs b/vsintegration/src/FSharp.LanguageService/BackgroundRequests.fs index 2978196e3c..5c2aa632f3 100644 --- a/vsintegration/src/FSharp.LanguageService/BackgroundRequests.fs +++ b/vsintegration/src/FSharp.LanguageService/BackgroundRequests.fs @@ -195,15 +195,14 @@ type internal FSharpLanguageServiceBackgroundRequests_DEPRECATED // Type-checking let typedResults,aborted = - match interactiveChecker.CheckFileInProjectAllowingStaleCachedResults(parseResults,req.FileName,req.Timestamp,req.Text,checkOptions) |> Async.RunSynchronously with - | None -> None,false - | Some FSharpCheckFileAnswer.Aborted -> + match interactiveChecker.CheckFileInProject(parseResults,req.FileName,req.Timestamp,FSharp.Compiler.Text.SourceText.ofString(req.Text),checkOptions) |> Async.RunSynchronously with + | FSharpCheckFileAnswer.Aborted -> // isResultObsolete returned true during the type check. None,true - | Some (FSharpCheckFileAnswer.Succeeded results) -> Some results, false + | FSharpCheckFileAnswer.Succeeded results -> Some results, false sr := None - parseResults,typedResults,true,aborted,req.Timestamp + parseResults,typedResults,true,aborted,int64 req.Timestamp // Now that we have the parseResults, we can SetDependencyFiles(). // @@ -219,7 +218,9 @@ type internal FSharpLanguageServiceBackgroundRequests_DEPRECATED // Furthermore, if the project is out-of-date behave just as if we were notified dependency files changed. if outOfDateProjectFileNames.Contains(projectFileName) then interactiveChecker.InvalidateConfiguration(checkOptions) - interactiveChecker.CheckProjectInBackground(checkOptions) + interactiveChecker.ParseAndCheckProject(checkOptions) + |> Async.RunSynchronously + |> ignore outOfDateProjectFileNames.Remove(projectFileName) |> ignore else @@ -234,7 +235,9 @@ type internal FSharpLanguageServiceBackgroundRequests_DEPRECATED req.IsAborted <- aborted // On 'FullTypeCheck', send a message to the reactor to start the background compile for this project, just in case if req.Reason = BackgroundRequestReason.FullTypeCheck then - interactiveChecker.CheckProjectInBackground(checkOptions) + interactiveChecker.ParseAndCheckProject(checkOptions) + |> Async.RunSynchronously + |> ignore | Some typedResults -> // Post the parse errors. @@ -255,13 +258,15 @@ type internal FSharpLanguageServiceBackgroundRequests_DEPRECATED let scope = new FSharpIntellisenseInfo_DEPRECATED(parseResults, req.Line, req.Col, req.Snapshot, typedResults, projectSite, req.View, colorizer, getDocumentationBuilder(), provideMethodList) req.ResultIntellisenseInfo <- scope - req.ResultTimestamp <- resultTimestamp // This will be different from req.Timestamp when we're using stale results. + req.ResultTimestamp <- int resultTimestamp // This will be different from req.Timestamp when we're using stale results. req.ResultClearsDirtinessOfFile <- containsFreshFullTypeCheck // On 'FullTypeCheck', send a message to the reactor to start the background compile for this project, just in case if req.Reason = BackgroundRequestReason.FullTypeCheck then - interactiveChecker.CheckProjectInBackground(checkOptions) + interactiveChecker.ParseAndCheckProject(checkOptions) + |> Async.RunSynchronously + |> ignore // On 'QuickInfo', get the text for the quick info while we're off the UI thread, instead of doing it later if req.Reason = BackgroundRequestReason.QuickInfo then diff --git a/vsintegration/tests/Salsa/FSharpLanguageServiceTestable.fs b/vsintegration/tests/Salsa/FSharpLanguageServiceTestable.fs index 36620d2ef6..cddffc15c0 100644 --- a/vsintegration/tests/Salsa/FSharpLanguageServiceTestable.fs +++ b/vsintegration/tests/Salsa/FSharpLanguageServiceTestable.fs @@ -101,7 +101,6 @@ type internal FSharpLanguageServiceTestable() as this = match checkerContainerOpt with | Some container -> let checker = container - checker.StopBackgroundCompile() checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() | None -> () @@ -225,4 +224,4 @@ type internal FSharpLanguageServiceTestable() as this = // // This is for unit testing only member this.WaitForBackgroundCompile() = - this.FSharpChecker.WaitForBackgroundCompile() + () diff --git a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.Completion.fs b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.Completion.fs index af3a35a80f..118339a2d8 100644 --- a/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.Completion.fs +++ b/vsintegration/tests/UnitTests/LegacyLanguageService/Tests.LanguageService.Completion.fs @@ -4526,7 +4526,8 @@ let x = query { for bbbb in abbbbc(*D0*) do // Save file2 ReplaceFileInMemory file2 [""] - SaveFileToDisk file2 + SaveFileToDisk file2 + let file3 = OpenFile(project,"File3.fs") TakeCoffeeBreak(this.VS) gpatcc.AssertExactly(notAA[file2; file3], notAA[file2;file3]) @@ -5124,6 +5125,7 @@ let x = query { for bbbb in abbbbc(*D0*) do Assert.IsTrue(completions.Length>0) [] + [] member this.``BadCompletionAfterQuicklyTyping.Bug72561``() = let code = [ " " ] let (_, _, file) = this.CreateSingleFileProject(code) @@ -7148,7 +7150,6 @@ let rec f l = let (_, _, file) = this.CreateSingleFileProject(code) TakeCoffeeBreak(this.VS) - let gpatcc = GlobalParseAndTypeCheckCounter.StartNew(this.VS) // In this case, we quickly type "." and then get dot-completions // For "level <- Module" this shows completions from the "Module" (e.g. "Module.Other") @@ -7161,7 +7162,6 @@ let rec f l = let completions = AutoCompleteAtCursor file AssertCompListContainsAll(completions, ["Length"]) AssertCompListDoesNotContainAny(completions, ["AbstractClassAttribute"]) - gpatcc.AssertExactly(0,0) [] member this.``SelfParameter.InDoKeywordScope``() =