Header menu logo telplin

Motivation

The merits of signature files

Signature files have three significant benefits to an F# code base.

References assemblies

In dotnet 7, F# supports references assemblies.
These can be produced by adding <ProduceReferenceAssembly>true</ProduceReferenceAssembly> to your fsproj.

An important part of a reference assembly is the generated mvid.
This mvid should only change when the public API changes. Alas, this doesn't always work in F# code. Adding a new let private binding could potentially influence the mvid, even though the public API didn't change.
When signature files are used, the mvid does not change because the public API would only change when the signature changes.

Background checker speed up

When enablePartialTypeChecking is enabled in the F# Checker, your IDE will skip the typechecking of implementation files that are backed by a signature, when type information is requested for a file.

So imagine the following file structure:

A.fsi
A.fs
B.fsi
B.fs
C.fs
D.fs

If you open file D.fs, your editor will request type information for D.fs and it will need to know what happened in all the files prior to D.fs.
As signature files expose all the same information, the background compiler can skip over A.fs and B.fs. Because A.fsi and B.fsi, will contain the same information.
This improvement can make the IDE feel a lot snappier when working in a large codebase.

Compilation improvement

In dotnet/fsharp#14494, a new feature was introduced to optimize the compiler. If an implementation file is backed by a signature file, the verification of whether the implementation and its signature match will be done in parallel.
If a file relies on a backed file as a dependency, it can leverage the signature information to perform self-type checking. This approach not only improves efficiency but also significantly speeds up the type-checking process compared to checking the implementation file alone.
This feature will be part of dotnet 7.0.4xx and can be enabled by adding <OtherFlags>--test:GraphBasedChecking</OtherFlags> to your fsproj.

Why this tool?

The F# compiler currently exposes a feature to generate signature files during a build.
This can be enabled by adding <OtherFlags>--allsigs</OtherFlags> to your fsproj.

So why introduce an alternative for this?

Typed tree only

--allsigs will generate a signature file based on the typed tree. This leads to some rather mixed results when you compare it to your implementation file.

Example:

module MyNamespace.MyModule

open System
open System.Collections.Generic

[<Literal>]
let Warning = "Some warning"

type Foo() =
    [<Obsolete(Warning)>]
    member this.Bar(x: int) = 0

    member this.Barry(x: int, y: int) = x + y
    member this.CollectKeys(d: IDictionary<string, string>) = d.Keys

Leads to

namespace MyNamespace
    
    module MyModule =
        
        [<Literal>]
        val Warning: string = "Some warning"
        
        type Foo =
            
            new: unit -> Foo
            
            [<System.Obsolete ("Some warning")>]
            member Bar: x: int -> int
            
            member Barry: x: int * y: int -> int
            
            member
              CollectKeys: d: System.Collections.Generic.IDictionary<string,
                                                                     string>
                             -> System.Collections.Generic.ICollection<string>

Syntactically this is a correct signature file, however, it is quite the departure from the source material.
The typed tree misses a lot of context the implementation file has.

Telplin works a bit different and tries to remain as faithful as possible to the original implementation file using both the untyped and the typed syntax tree.

Faster release cycle.

As the --allsigs flag is part of the F# compiler, this means that potential fixes to this feature are tied to dotnet SDK releases.
The release cadence of the dotnet SDK can be somewhat unpredictable and it could take a while before a fix finally reaches end-users.

Telplin is a standalone tool that should be able to ship fixes shortly after they got merged.

namespace System
namespace System.Collections
namespace System.Collections.Generic
Multiple items
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute

--------------------
new: unit -> LiteralAttribute
Multiple items
type Foo = new: obj -> Foo member Bar: (int -> int) member Barry: (int * int -> int) member CollectKeys: (IDictionary<string,string> -> ICollection<string>)

--------------------
new: obj -> Foo
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
type unit = Unit
Multiple items
type ObsoleteAttribute = inherit Attribute new: unit -> unit + 2 overloads member DiagnosticId: string member IsError: bool member Message: string member UrlFormat: string
<summary>Marks the program elements that are no longer in use. This class cannot be inherited.</summary>

--------------------
System.ObsoleteAttribute() : System.ObsoleteAttribute
System.ObsoleteAttribute(message: string) : System.ObsoleteAttribute
System.ObsoleteAttribute(message: string, error: bool) : System.ObsoleteAttribute
type IDictionary<'TKey,'TValue> = inherit ICollection<KeyValuePair<'TKey,'TValue>> inherit IEnumerable<KeyValuePair<'TKey,'TValue>> inherit IEnumerable override Add: key: 'TKey * value: 'TValue -> unit override ContainsKey: key: 'TKey -> bool override Remove: key: 'TKey -> bool override TryGetValue: key: 'TKey * value: byref<'TValue> -> bool member Item: 'TValue member Keys: ICollection<'TKey> member Values: ICollection<'TValue>
<summary>Represents a generic collection of key/value pairs.</summary>
<typeparam name="TKey">The type of keys in the dictionary.</typeparam>
<typeparam name="TValue">The type of values in the dictionary.</typeparam>
type ICollection<'T> = inherit IEnumerable<'T> inherit IEnumerable override Add: item: 'T -> unit override Clear: unit -> unit override Contains: item: 'T -> bool override CopyTo: array: 'T array * arrayIndex: int -> unit override Remove: item: 'T -> bool member Count: int member IsReadOnly: bool
<summary>Defines methods to manipulate generic collections.</summary>
<typeparam name="T">The type of the elements in the collection.</typeparam>

Type something to start searching.