From f48c44e38b847c29b444b3b1a29250ebfe089c07 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:12:48 +0200 Subject: [PATCH 1/3] parallel reference resolution with deterministic order --- src/Compiler/Driver/CompilerImports.fs | 143 ++++++++++++------------- 1 file changed, 66 insertions(+), 77 deletions(-) diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs index 4ab1ca3d7e..55001c7092 100644 --- a/src/Compiler/Driver/CompilerImports.fs +++ b/src/Compiler/Driver/CompilerImports.fs @@ -2247,104 +2247,93 @@ and [] TcImports 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) - : Async<(_ * (unit -> AvailableImportedAssembly list)) option> = - async { - CheckDisposed() - let m = r.originalReference.Range - let fileName = r.resolvedPath + member tcImports.RegisterAndImportReferencedAssemblies(ctok, nms: AssemblyResolution list) = + let tryGetAssemblyData (r: AssemblyResolution) = + async { + CheckDisposed() + let m = r.originalReference.Range + let fileName = r.resolvedPath - let! contentsOpt = - async { - match r.ProjectReference with - | Some ilb -> return! ilb.EvaluateRawContents() - | None -> return ProjectAssemblyDataResult.Unavailable true - } + try - // If we have a project reference but did not get any valid contents, - // just return None and do not attempt to read elsewhere. - match contentsOpt with - | ProjectAssemblyDataResult.Unavailable false -> return None - | _ -> + let! contentsOpt = + async { + match r.ProjectReference with + | Some ilb -> return! ilb.EvaluateRawContents() + | None -> return ProjectAssemblyDataResult.Unavailable true + } - let assemblyData = + // If we have a project reference but did not get any valid contents, + // just return None and do not attempt to read elsewhere. match contentsOpt with - | ProjectAssemblyDataResult.Available ilb -> ilb - | ProjectAssemblyDataResult.Unavailable _ -> - let ilModule, ilAssemblyRefs = tcImports.OpenILBinaryModule(ctok, fileName, m) - RawFSharpAssemblyDataBackedByFileOnDisk(ilModule, ilAssemblyRefs) :> IRawFSharpAssemblyData + | ProjectAssemblyDataResult.Unavailable false -> return None + | _ -> + + match contentsOpt with + | ProjectAssemblyDataResult.Available ilb -> return Some(r, ilb) + | ProjectAssemblyDataResult.Unavailable _ -> + let ilModule, ilAssemblyRefs = tcImports.OpenILBinaryModule(ctok, fileName, m) + return Some(r, RawFSharpAssemblyDataBackedByFileOnDisk(ilModule, ilAssemblyRefs)) + + with e -> + errorR (Error(FSComp.SR.buildProblemReadingAssembly (fileName, e.Message), m)) + return None + } - let ilShortAssemName = assemblyData.ShortAssemblyName - let ilScopeRef = assemblyData.ILScopeRef + let registerDll (r: AssemblyResolution, assemblyData: IRawFSharpAssemblyData) = + let m = r.originalReference.Range + let fileName = r.resolvedPath + let ilShortAssemName = assemblyData.ShortAssemblyName + let ilScopeRef = assemblyData.ILScopeRef - if tcImports.IsAlreadyRegistered ilShortAssemName then - let dllinfo = tcImports.FindDllInfo(ctok, m, ilShortAssemName) + if tcImports.IsAlreadyRegistered ilShortAssemName then - let phase2 () = - [ tcImports.FindCcuInfo(ctok, m, ilShortAssemName, lookupOnly = true) ] + let phase2 () = + [ tcImports.FindCcuInfo(ctok, m, ilShortAssemName, lookupOnly = true) ] - return Some(dllinfo, phase2) - else - let dllinfo = - { - RawMetadata = assemblyData - FileName = fileName + phase2 + else + let dllinfo = + { + RawMetadata = assemblyData + FileName = fileName #if !NO_TYPEPROVIDERS - ProviderGeneratedAssembly = None - IsProviderGenerated = false - ProviderGeneratedStaticLinkMap = None + ProviderGeneratedAssembly = None + IsProviderGenerated = false + ProviderGeneratedStaticLinkMap = None #endif - ILScopeRef = ilScopeRef - ILAssemblyRefs = assemblyData.ILAssemblyRefs - } + ILScopeRef = ilScopeRef + ILAssemblyRefs = assemblyData.ILAssemblyRefs + } - tcImports.RegisterDll dllinfo + tcImports.RegisterDll dllinfo - let phase2 = - if assemblyData.HasAnyFSharpSignatureDataAttribute then - if not assemblyData.HasMatchingFSharpSignatureDataAttribute then - errorR (Error(FSComp.SR.buildDifferentVersionMustRecompile fileName, m)) - tcImports.PrepareToImportReferencedILAssembly(ctok, m, fileName, dllinfo) - else - try - tcImports.PrepareToImportReferencedFSharpAssembly(ctok, m, fileName, dllinfo) - with e -> - error (Error(FSComp.SR.buildErrorOpeningBinaryFile (fileName, e.Message), m)) - else + let phase2 = + if assemblyData.HasAnyFSharpSignatureDataAttribute then + if not assemblyData.HasMatchingFSharpSignatureDataAttribute then + errorR (Error(FSComp.SR.buildDifferentVersionMustRecompile fileName, m)) tcImports.PrepareToImportReferencedILAssembly(ctok, m, fileName, dllinfo) + else + try + tcImports.PrepareToImportReferencedFSharpAssembly(ctok, m, fileName, dllinfo) + with e -> + error (Error(FSComp.SR.buildErrorOpeningBinaryFile (fileName, e.Message), m)) + else + tcImports.PrepareToImportReferencedILAssembly(ctok, m, fileName, dllinfo) - return Some(dllinfo, phase2) - } + phase2 - // 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) = async { CheckDisposed() - let tcConfig = tcConfigP.Get ctok + let! assemblyData = nms |> List.map tryGetAssemblyData |> MultipleDiagnosticsLoggers.Parallel - let runMethod = - match tcConfig.parallelReferenceResolution with - | ParallelReferenceResolution.On -> MultipleDiagnosticsLoggers.Parallel - | ParallelReferenceResolution.Off -> MultipleDiagnosticsLoggers.Sequential + // Preserve determinicstic order of references, because types from later assemblies may shadow earlier ones. + let phase2s = assemblyData |> Seq.choose id |> Seq.map registerDll |> List.ofSeq - let! results = - nms - |> List.map (fun nm -> - async { - try - use _ = new CompilationGlobalsScope() - return! tcImports.TryRegisterAndPrepareToImportReferencedDll(ctok, nm) - with e -> - errorR (Error(FSComp.SR.buildProblemReadingAssembly (nm.resolvedPath, e.Message), nm.originalReference.Range)) - return None - }) - |> runMethod - - let _dllinfos, phase2s = results |> Array.choose id |> List.ofArray |> List.unzip fixupOrphanCcus () - let ccuinfos = List.collect (fun phase2 -> phase2 ()) phase2s + + let ccuinfos = phase2s |> List.collect (fun phase2 -> phase2 ()) if importsBase.IsSome then importsBase.Value.CcuTable.Values |> Seq.iter addConstraintSources From 117d24505a1767c6c0663222a3afbc04877c1318 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:49:35 +0200 Subject: [PATCH 2/3] prevent fsi deadlock --- src/Compiler/Driver/CompilerImports.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs index 55001c7092..7a5bbd3c9a 100644 --- a/src/Compiler/Driver/CompilerImports.fs +++ b/src/Compiler/Driver/CompilerImports.fs @@ -2365,7 +2365,7 @@ and [] TcImports ReportWarnings warns tcImports.RegisterAndImportReferencedAssemblies(ctok, res) - |> Async.RunImmediate + |> Async.RunSynchronously |> ignore true @@ -2673,7 +2673,7 @@ let RequireReferences (ctok, tcImports: TcImports, tcEnv, thisAssemblyName, reso let ccuinfos = tcImports.RegisterAndImportReferencedAssemblies(ctok, resolutions) - |> Async.RunImmediate + |> Async.RunSynchronously let asms = ccuinfos From 86fbf1bb1fb98f1c0e345d8df1c9fefe83cfb16a Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:06:02 +0200 Subject: [PATCH 3/3] phase2, too --- src/Compiler/Driver/CompilerImports.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs index 7a5bbd3c9a..a9d1d0cba4 100644 --- a/src/Compiler/Driver/CompilerImports.fs +++ b/src/Compiler/Driver/CompilerImports.fs @@ -2291,7 +2291,7 @@ and [] TcImports let phase2 () = [ tcImports.FindCcuInfo(ctok, m, ilShortAssemName, lookupOnly = true) ] - phase2 + async { return phase2 () } else let dllinfo = { @@ -2321,7 +2321,7 @@ and [] TcImports else tcImports.PrepareToImportReferencedILAssembly(ctok, m, fileName, dllinfo) - phase2 + async { return phase2 () } async { CheckDisposed() @@ -2333,13 +2333,13 @@ and [] TcImports fixupOrphanCcus () - let ccuinfos = phase2s |> List.collect (fun phase2 -> phase2 ()) + let! ccuinfos = phase2s |> MultipleDiagnosticsLoggers.Parallel if importsBase.IsSome then importsBase.Value.CcuTable.Values |> Seq.iter addConstraintSources ccuTable.Values |> Seq.iter addConstraintSources - return ccuinfos + return ccuinfos |> List.concat } /// Note that implicit loading is not used for compilations from MSBuild, which passes ``--noframework``