Skip to content

Instantly share code, notes, and snippets.

@nojaf
Created September 21, 2022 14:39
Show Gist options
  • Save nojaf/0229e9fdb77e2b736839ddd5d15af8f9 to your computer and use it in GitHub Desktop.
Save nojaf/0229e9fdb77e2b736839ddd5d15af8f9 to your computer and use it in GitHub Desktop.
The benefit of signature files in FCS FSharpChecker

Hello,

I've done some digging into the benefits of having a signature file in your editor.

The FSharpChecker has a setting FSharpChecker.Create(enablePartialTypeChecking = true). Rider enables this via:

https://github.com/JetBrains/resharper-fsharp/blob/c60b2d1d3a36044861eb1a6abd1f646201994f9e/ReSharper.FSharp/src/FSharp.Common/src/Checker/FcsCheckerService.fs#L77

image

When you invoke checker.ParseAndCheckFileInProject, the backgroundChecker will construct a BoundModel inside an IncrementalBuilder :

service.fs:

bc.ParseAndCheckFileInProject -> getOrCreateBuilder -> createBuilderNode -> CreateOneIncrementalBuilder -> IncrementalBuilder.TryCreateIncrementalBuilderForProjectOptions

IncrementalBuilder.fs:

TryCreateIncrementalBuilderForProjectOptions (takes enablePartialTypeChecking:bool) -> IncrementalBuilderState.Create(initialState)

This IncrementalBuilderState.Create will create a BoundModel for each fsharp file. https://github.com/dotnet/fsharp/blob/f4f0e7e5173f525b4c3ff6476d82f60c9f01fbf4/src/Compiler/Service/IncrementalBuild.fs#L1146-L1147

Using IncrementalBuilderStateHelpers.createBoundModelGraphNode, which will construct a GraphNode that calls IncrementalBuilderHelpers.TypeCheckTask.

This TypeCheckTask will call prevBoundModel.Next for the current file the evalutation of the GraphNode will call BoundModel.TypeCheck. In TypeCheckTask, we also see prevBoundModel.Next which goes to the next file.

https://github.com/dotnet/fsharp/blob/f4f0e7e5173f525b4c3ff6476d82f60c9f01fbf4/src/Compiler/Service/IncrementalBuild.fs#L455-L569

BoundModel calls private TypeCheck in the constructor, we see our first glimps of enablePartialTypeChecking (named partialCheck).

let sigNameOpt =
    if partialCheck then
        this.BackingSignature
    else
        None

If you have a signature file: syntaxTree.Parse will return:

                let canSkip = sigNameOpt.IsSome && FSharpImplFileSuffixes |> List.exists (FileSystemUtils.checkSuffix fileName)
                let input =
                    if canSkip then
                        ParsedInput.ImplFile(
                            ParsedImplFileInput(
                                fileName,
                                false,
                                sigNameOpt.Value,
                                [],
                                [],
                                [],
                                isLastCompiland,
                                { ConditionalDirectives = []; CodeComments = [] }
                            )
                        )

https://github.com/dotnet/fsharp/blob/f4f0e7e5173f525b4c3ff6476d82f60c9f01fbf4/src/Compiler/Service/IncrementalBuild.fs#L118-L139

Notice the rather empty syntax tree. In CheckOneInput (CheckOneInputAux actually),

https://github.com/dotnet/fsharp/blob/a19238ec7451b4c0a5f4f67909b2026e2acfdb0a/src/Compiler/Driver/ParseAndCheckInputs.fs#L1338-L1368

                // Check if we've got an interface for this fragment
                let rootSigOpt = tcState.tcsRootSigs.TryFind qualNameOfFile

They will try and find the signature type infomation based on the qualNameOfFile.

If that is found (and the setting is active), the TypeChecking will be skipped entirely:

                match rootSigOpt with
                | Some rootSig when skipImplIfSigExists ->
                    // Delay the typecheck the implementation file until the second phase of parallel processing.
                    // Adjust the TcState as if it has been checked, which makes the signature for the file available later
                    // in the compilation order.
                    let tcStateForImplFile = tcState
                    let qualNameOfFile = file.QualifiedName
                    let priorErrors = checkForErrors ()

                    let ccuSigForFile, tcState =
                        AddCheckResultsToTcState
                            (tcGlobals, amap, hadSig, prefixPathOpt, tcSink, tcState.tcsTcImplEnv, qualNameOfFile, rootSig)
                            tcState

                    let partialResult =
                        (amap, conditionalDefines, rootSig, priorErrors, file, tcStateForImplFile, ccuSigForFile)

                    return Choice2Of2 partialResult, tcState

CheckOneInputAux is also being called from CheckMultipleInputsInParallel in the parallal PR:

https://github.com/nojaf/fsharp/blob/a19238ec7451b4c0a5f4f67909b2026e2acfdb0a/src/Compiler/Driver/ParseAndCheckInputs.fs#L1427-L1482

And that somewhat explains the benefit of signature files both in the FSharpChecker and during compilation.

Did you have enablePartialTypeChecking = true in the benchmark test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment