From f93b7f47028358b101feb1c3a79e72a4f38dbbcd Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Thu, 13 Aug 2026 12:03:05 +0200 Subject: [PATCH 1/5] IL: share ILCallingConv instances There are only 18 combinations of ILThisConvention and ILArgConvention. Pre-create all of them and hand out the shared instance from ILCallingConv.Create, so reading a method signature no longer allocates a Callconv. The two existing statics (Instance, Static) are placed in the table too, so every use shares one instance per combination. The index is computed with explicit matches rather than by casting the union tags, so adding a case to either union is a compile error here instead of a bad index. Co-Authored-By: Claude Opus 5 (1M context) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/AbstractIL/il.fs | 47 +++++++++++++++++++ src/Compiler/AbstractIL/il.fsi | 4 ++ src/Compiler/AbstractIL/ilread.fs | 2 +- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 38dd2d65b0a..50928b936df 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -163,6 +163,7 @@ * Expand `` at tooling time. In IDE tooltips, completion, and signature help, documentation is inherited from base classes, interfaces, overridden members, and constructors (matched by parameter signature). The FCS Symbols API (`FSharpSymbol.XmlDoc`) additionally resolves explicit `cref` targets, but does not expand constructor inheritance. The compiler emits the tag verbatim into generated XML documentation files, matching C#; `` is not implemented. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) * Add symbol and type highlighting to F# diagnostics ([PR #20097](https://github.com/dotnet/fsharp/pull/20097)) * IL: add `ILPreNamespace`, make `ILPreTypeDef` creation lazy ([PR #20092](https://github.com/dotnet/fsharp/pull/20092)) +* IL: share `ILCallingConv` instances. There are only 18 combinations of `ILThisConvention` and `ILArgConvention`, so they are all pre-created and `ILCallingConv.Create` hands out the shared instance, instead of `ilread` allocating a fresh `Callconv` per method signature. ### Improved diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 0aa4e76ecf4..5635f92b2c4 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -683,6 +683,8 @@ type ILCallingConv = static member Static = ILCallingConvStatics.Static + static member Create(thisConv, argConv) = ILCallingConvStatics.Get(thisConv, argConv) + override x.ToString() = if x.IsStatic then "static" else "instance" @@ -693,10 +695,55 @@ and ILCallingConvStatics() = static let staticCallConv = Callconv(ILThisConvention.Static, ILArgConvention.Default) + /// Every combination, so that reading metadata never allocates a calling convention. The two + /// common ones above are placed in the table too, so all uses share one instance per combination. + static let allCallConvs = + let thisConvs = + [| + ILThisConvention.Instance + ILThisConvention.InstanceExplicit + ILThisConvention.Static + |] + + let argConvs = + [| + ILArgConvention.Default + ILArgConvention.CDecl + ILArgConvention.StdCall + ILArgConvention.ThisCall + ILArgConvention.FastCall + ILArgConvention.VarArg + |] + + Array.init (thisConvs.Length * argConvs.Length) (fun i -> + match thisConvs[i / argConvs.Length], argConvs[i % argConvs.Length] with + | ILThisConvention.Instance, ILArgConvention.Default -> instanceCallConv + | ILThisConvention.Static, ILArgConvention.Default -> staticCallConv + | thisConv, argConv -> Callconv(thisConv, argConv)) + static member Instance = instanceCallConv static member Static = staticCallConv + static member Get(thisConv, argConv) = + // Explicit, so adding a case to either union is a compile error here rather than a bad index. + let thisIdx = + match thisConv with + | ILThisConvention.Instance -> 0 + | ILThisConvention.InstanceExplicit -> 1 + | ILThisConvention.Static -> 2 + + let argIdx = + match argConv with + | ILArgConvention.Default -> 0 + | ILArgConvention.CDecl -> 1 + | ILArgConvention.StdCall -> 2 + | ILArgConvention.ThisCall -> 3 + | ILArgConvention.FastCall -> 4 + | ILArgConvention.VarArg -> 5 + + allCallConvs[thisIdx * 6 + argIdx] + type ILBoxity = | AsObject | AsValue diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index 8e82bd176fb..a2accae5973 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -186,6 +186,10 @@ type ILCallingConv = static member Instance: ILCallingConv static member Static: ILCallingConv + /// Returns the shared instance for this combination. There are only 18, so readers should use + /// this instead of allocating a fresh 'Callconv' per method signature. + static member internal Create: ILThisConvention * ILArgConvention -> ILCallingConv + /// Array shapes. For most purposes the rank is the only thing that matters. type internal ILArrayBound = int32 option diff --git a/src/Compiler/AbstractIL/ilread.fs b/src/Compiler/AbstractIL/ilread.fs index 0ccdd9cf35c..fb0d91742b5 100644 --- a/src/Compiler/AbstractIL/ilread.fs +++ b/src/Compiler/AbstractIL/ilread.fs @@ -2835,7 +2835,7 @@ and byteAsCallConv b = ILArgConvention.Default let generic = (b &&& e_IMAGE_CEE_CS_CALLCONV_GENERIC) <> 0x0uy - generic, Callconv(byteAsHasThis b, cc) + generic, ILCallingConv.Create(byteAsHasThis b, cc) and seekReadMemberRefAsMethodData ctxt numTypars idx : VarArgMethodData = ctxt.seekReadMemberRefAsMethodData (MemberRefAsMspecIdx(numTypars, idx)) From e8da2aa1be3979efc574d00b2df3479473b919c4 Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Thu, 13 Aug 2026 12:56:47 +0200 Subject: [PATCH 2/5] Release notes --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 50928b936df..b247803ffcc 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -164,6 +164,7 @@ * Add symbol and type highlighting to F# diagnostics ([PR #20097](https://github.com/dotnet/fsharp/pull/20097)) * IL: add `ILPreNamespace`, make `ILPreTypeDef` creation lazy ([PR #20092](https://github.com/dotnet/fsharp/pull/20092)) * IL: share `ILCallingConv` instances. There are only 18 combinations of `ILThisConvention` and `ILArgConvention`, so they are all pre-created and `ILCallingConv.Create` hands out the shared instance, instead of `ilread` allocating a fresh `Callconv` per method signature. +* IL: share ILCallingConv instances ([PR #20254](https://github.com/dotnet/fsharp/pull/20254) ### Improved From 4c151a363b880451f31523df750c9966cae19fa6 Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Thu, 13 Aug 2026 12:57:17 +0200 Subject: [PATCH 3/5] Make Create public --- src/Compiler/AbstractIL/il.fsi | 2 +- .../FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index a2accae5973..409f11ca42c 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -188,7 +188,7 @@ type ILCallingConv = /// Returns the shared instance for this combination. There are only 18, so readers should use /// this instead of allocating a fresh 'Callconv' per method signature. - static member internal Create: ILThisConvention * ILArgConvention -> ILCallingConv + static member Create: ILThisConvention * ILArgConvention -> ILCallingConv /// Array shapes. For most purposes the rank is the only thing that matters. type internal ILArrayBound = int32 option diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 055808bf332..08ede2ab992 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -321,6 +321,7 @@ FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean Equals(System.Object) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean Equals(System.Object, System.Collections.IEqualityComparer) FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILArgConvention Item2 FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILArgConvention get_Item2() +FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv Create(ILThisConvention, ILArgConvention) FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv Instance FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv NewCallconv(ILThisConvention, ILArgConvention) FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv Static From 267a10248f9595c5dd2f4312712d814c152561b5 Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Thu, 13 Aug 2026 13:25:56 +0200 Subject: [PATCH 4/5] Force using Create --- src/Compiler/AbstractIL/il.fsi | 15 ++++++++------- src/Compiler/AbstractIL/ilpars.fsy | 6 +++--- src/Compiler/AbstractIL/ilprint.fs | 6 +++--- src/Compiler/AbstractIL/ilreflect.fs | 6 +++--- src/Compiler/AbstractIL/ilwrite.fs | 6 +++--- src/Compiler/TypedTree/TypedTreePickle.fs | 6 +++--- ...mpiler.Service.SurfaceArea.netstandard20.bsl | 17 ++++++++++------- .../ModuleReaderCancellationTests.fs | 2 +- 8 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index 409f11ca42c..aa2e7c69b2f 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -175,19 +175,20 @@ type ILThisConvention = [] type ILCallingConv = + private | Callconv of ILThisConvention * ILArgConvention - member internal IsInstance: bool - member internal IsInstanceExplicit: bool - member internal IsStatic: bool - member internal ThisConv: ILThisConvention - member internal BasicConv: ILArgConvention + member IsInstance: bool + member IsInstanceExplicit: bool + member IsStatic: bool + member ThisConv: ILThisConvention + member BasicConv: ILArgConvention static member Instance: ILCallingConv static member Static: ILCallingConv - /// Returns the shared instance for this combination. There are only 18, so readers should use - /// this instead of allocating a fresh 'Callconv' per method signature. + /// Returns the shared instance for this combination. Since the representation is private and there + /// are only 18 combinations, no calling convention is ever allocated per method signature. static member Create: ILThisConvention * ILArgConvention -> ILCallingConv /// Array shapes. For most purposes the rank is the only thing that matters. diff --git a/src/Compiler/AbstractIL/ilpars.fsy b/src/Compiler/AbstractIL/ilpars.fsy index ca06f6570be..1b5417e24be 100644 --- a/src/Compiler/AbstractIL/ilpars.fsy +++ b/src/Compiler/AbstractIL/ilpars.fsy @@ -189,11 +189,11 @@ typSpec: callConv: INSTANCE callKind - { Callconv (ILThisConvention.Instance,$2) } + { ILCallingConv.Create (ILThisConvention.Instance,$2) } | EXPLICIT callKind - { Callconv (ILThisConvention.InstanceExplicit,$2) } + { ILCallingConv.Create (ILThisConvention.InstanceExplicit,$2) } | callKind - { Callconv (ILThisConvention.Static,$1) } + { ILCallingConv.Create (ILThisConvention.Static,$1) } callKind: /* EMPTY */ diff --git a/src/Compiler/AbstractIL/ilprint.fs b/src/Compiler/AbstractIL/ilprint.fs index f1fb38174c9..0a892c76d3d 100644 --- a/src/Compiler/AbstractIL/ilprint.fs +++ b/src/Compiler/AbstractIL/ilprint.fs @@ -365,15 +365,15 @@ and output_bcc os bcc = | ILArgConvention.Default -> " " | ILArgConvention.VarArg -> "vararg ") -and output_callconv os (Callconv(hasthis, cc)) = +and output_callconv os (callconv: ILCallingConv) = output_string os - (match hasthis with + (match callconv.ThisConv with | ILThisConvention.Instance -> "instance " | ILThisConvention.InstanceExplicit -> "explicit " | ILThisConvention.Static -> "") - output_bcc os cc + output_bcc os callconv.BasicConv and goutput_dlocref env os (dref: ILType) = match dref with diff --git a/src/Compiler/AbstractIL/ilreflect.fs b/src/Compiler/AbstractIL/ilreflect.fs index 9aa9d6403a0..78b1fdf8b67 100644 --- a/src/Compiler/AbstractIL/ilreflect.fs +++ b/src/Compiler/AbstractIL/ilreflect.fs @@ -694,15 +694,15 @@ let envPopEntryPts emEnv = // convCallConv //---------------------------------------------------------------------------- -let convCallConv (Callconv(hasThis, basic)) = +let convCallConv (callConv: ILCallingConv) = let ccA = - match hasThis with + match callConv.ThisConv with | ILThisConvention.Static -> CallingConventions.Standard | ILThisConvention.InstanceExplicit -> CallingConventions.ExplicitThis | ILThisConvention.Instance -> CallingConventions.HasThis let ccB = - match basic with + match callConv.BasicConv with | ILArgConvention.Default -> enum 0 | ILArgConvention.CDecl -> enum 0 | ILArgConvention.StdCall -> enum 0 diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs index f626e1e56ef..b974e814574 100644 --- a/src/Compiler/AbstractIL/ilwrite.fs +++ b/src/Compiler/AbstractIL/ilwrite.fs @@ -838,10 +838,10 @@ let hasthisToByte hasthis = | ILThisConvention.InstanceExplicit -> e_IMAGE_CEE_CS_CALLCONV_INSTANCE_EXPLICIT | ILThisConvention.Static -> 0x00uy -let callconvToByte ntypars (Callconv (hasthis, bcc)) = - hasthisToByte hasthis ||| +let callconvToByte ntypars (callconv: ILCallingConv) = + hasthisToByte callconv.ThisConv ||| (if ntypars > 0 then e_IMAGE_CEE_CS_CALLCONV_GENERIC else 0x00uy) ||| - (match bcc with + (match callconv.BasicConv with | ILArgConvention.FastCall -> e_IMAGE_CEE_CS_CALLCONV_FASTCALL | ILArgConvention.StdCall -> e_IMAGE_CEE_CS_CALLCONV_STDCALL | ILArgConvention.ThisCall -> e_IMAGE_CEE_CS_CALLCONV_THISCALL diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index 5b64b10f600..1206d7afd7c 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -1285,8 +1285,8 @@ and p_ILBasicCallConv x st = | ILArgConvention.VarArg -> 5) st -and p_ILCallConv (Callconv(x, y)) st = - p_tup2 p_ILHasThis p_ILBasicCallConv (x, y) st +and p_ILCallConv (x: ILCallingConv) st = + p_tup2 p_ILHasThis p_ILBasicCallConv (x.ThisConv, x.BasicConv) st and p_ILCallSig x st = p_tup3 p_ILCallConv p_ILTypes p_ILType (x.CallingConv, x.ArgTypes, x.ReturnType) st @@ -1316,7 +1316,7 @@ let u_ILHasThis st = let u_ILCallConv st = let a, b = u_tup2 u_ILHasThis u_ILBasicCallConv st - Callconv(a, b) + ILCallingConv.Create(a, b) let u_ILTypeRef st = let a, b, c = u_tup3 u_ILScopeRef u_strings u_string st diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 08ede2ab992..ba8d1825ece 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -319,23 +319,26 @@ FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean Equals(ILCallingConv) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean Equals(ILCallingConv, System.Collections.IEqualityComparer) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean Equals(System.Object) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean Equals(System.Object, System.Collections.IEqualityComparer) -FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILArgConvention Item2 -FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILArgConvention get_Item2() +FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean IsInstance +FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean IsInstanceExplicit +FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean IsStatic +FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean get_IsInstance() +FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean get_IsInstanceExplicit() +FSharp.Compiler.AbstractIL.IL+ILCallingConv: Boolean get_IsStatic() +FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILArgConvention BasicConv +FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILArgConvention get_BasicConv() FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv Create(ILThisConvention, ILArgConvention) FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv Instance -FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv NewCallconv(ILThisConvention, ILArgConvention) FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv Static FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv get_Instance() FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILCallingConv get_Static() -FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILThisConvention Item1 -FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILThisConvention get_Item1() +FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILThisConvention ThisConv +FSharp.Compiler.AbstractIL.IL+ILCallingConv: ILThisConvention get_ThisConv() FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 CompareTo(ILCallingConv) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 CompareTo(System.Object) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 CompareTo(System.Object, System.Collections.IComparer) FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 GetHashCode() FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 GetHashCode(System.Collections.IEqualityComparer) -FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 Tag -FSharp.Compiler.AbstractIL.IL+ILCallingConv: Int32 get_Tag() FSharp.Compiler.AbstractIL.IL+ILCallingConv: System.String ToString() FSharp.Compiler.AbstractIL.IL+ILCallingSignature: Boolean Equals(ILCallingSignature) FSharp.Compiler.AbstractIL.IL+ILCallingSignature: Boolean Equals(ILCallingSignature, System.Collections.IEqualityComparer) diff --git a/tests/FSharp.Compiler.Service.Tests/ModuleReaderCancellationTests.fs b/tests/FSharp.Compiler.Service.Tests/ModuleReaderCancellationTests.fs index 473a503ba13..fc3641811a5 100644 --- a/tests/FSharp.Compiler.Service.Tests/ModuleReaderCancellationTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/ModuleReaderCancellationTests.fs @@ -50,7 +50,7 @@ module ModuleReader = MethodAttributes.NewSlot ||| MethodAttributes.SpecialName - let callingConv = Callconv(ILThisConvention.Instance, ILArgConvention.Default) + let callingConv = ILCallingConv.Instance let parameters = [] let ret = mkILReturn ILType.Void let genericParams = [] From eeff5f0a173c2735aecdec886032bf16dcd90861 Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Thu, 13 Aug 2026 13:27:24 +0200 Subject: [PATCH 5/5] Fantomas --- src/Compiler/AbstractIL/il.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 5635f92b2c4..8b7eeb25aee 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -683,7 +683,8 @@ type ILCallingConv = static member Static = ILCallingConvStatics.Static - static member Create(thisConv, argConv) = ILCallingConvStatics.Get(thisConv, argConv) + static member Create(thisConv, argConv) = + ILCallingConvStatics.Get(thisConv, argConv) override x.ToString() = if x.IsStatic then "static" else "instance"