Skip to content

Commit 567e53f

Browse files
More string optimizations (#18546)
This special-casing allows us to update FSharp.Core to avoid boxing when caling the `string` function on enums and signed integral types going forward while still allowing the updated version of FSharp.Core to be fully compatible with older compilers. Adding support for some form of constraint in library-only static optimizations instead would have been problematic for multiple reasons. Supporting something like `when 'T : enum<'U>` would have required additional modifications to the compiler and would not have been consumable by older compilers. It would also introduce a new type variable. While something like `when 'T : 'T & #Enum` is already syntactically valid, it would add that constraint to the entire `string` function without further modification to the typechecker. It would also not be consumable by older compilers. I think adding a special case for enums is justifiable since (1) enums are a special kind of type to begin with, and (2) static optimization constraints are only allowed in FSharp.Core, so the change to the language itself is quite small.
1 parent 942de60 commit 567e53f

21 files changed

+900
-90
lines changed

docs/release-notes/.FSharp.Compiler.Service/10.0.100.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
* Shorthand lambda: fix completion for chained calls and analysis for unfinished expression ([PR #18560](https://github.com/dotnet/fsharp/pull/18560))
1313
* Completion: fix previous namespace considered opened [PR #18609](https://github.com/dotnet/fsharp/pull/18609)
1414

15+
### Added
16+
17+
* Add support for `when 'T : Enum` library-only library-only static optimization constraint. ([PR #18546](https://github.com/dotnet/fsharp/pull/18546))
18+
1519
### Breaking Changes
1620

1721
* Scoped Nowarn: Add the #warnon compiler directive ([Language suggestion #278](https://github.com/fsharp/fslang-suggestions/issues/278), [RFC FS-1146 PR](https://github.com/fsharp/fslang-design/pull/782), [PR #18049](https://github.com/dotnet/fsharp/pull/18049))

docs/release-notes/.FSharp.Core/10.0.100.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
### Added
44

5+
* Enable more `string` optimizations by adding `when 'T : Enum` library-only library-only static optimization constraint. ([PR #18546](https://github.com/dotnet/fsharp/pull/18546))
6+
57
### Changed
68

79
* Random functions support for zero element chosen/sampled ([PR #18568](https://github.com/dotnet/fsharp/pull/18568))
810

9-
### Breaking Changes
11+
### Breaking Changes

src/Compiler/TypedTree/TcGlobals.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,8 @@ type TcGlobals(
12781278

12791279
member val ArrayCollector_tcr = mk_MFCompilerServices_tcref fslibCcu "ArrayCollector`1"
12801280

1281+
member val SupportsWhenTEnum_tcr = mk_MFCompilerServices_tcref fslibCcu "SupportsWhenTEnum"
1282+
12811283
member _.TryEmbedILType(tref: ILTypeRef, mkEmbeddableType: unit -> ILTypeDef) =
12821284
if tref.Scope = ILScopeRef.Local && not(embeddedILTypeDefs.ContainsKey(tref.Name)) then
12831285
embeddedILTypeDefs.TryAdd(tref.Name, mkEmbeddableType()) |> ignore

src/Compiler/TypedTree/TcGlobals.fsi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ type internal TcGlobals =
276276

277277
member ListCollector_tcr: FSharp.Compiler.TypedTree.EntityRef
278278

279+
member SupportsWhenTEnum_tcr: FSharp.Compiler.TypedTree.EntityRef
280+
279281
member MatchFailureException_tcr: FSharp.Compiler.TypedTree.EntityRef
280282

281283
member ResumableCode_tcr: FSharp.Compiler.TypedTree.EntityRef

src/Compiler/TypedTree/TypedTreeOps.fs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5801,11 +5801,23 @@ type StaticOptimizationAnswer =
58015801
// ^T : ^T --> used in (+), (-) etc. to guard witness-invoking implementations added in F# 5
58025802
// 'T : 'T --> used in FastGenericEqualityComparer, FastGenericComparer to guard struct/tuple implementations
58035803
//
5804+
// For performance and compatibility reasons, 'T when 'T is an enum is handled with its own special hack.
5805+
// Unlike for other 'T : tycon constraints, 'T can be any enum; it need not (and indeed must not) be identical to System.Enum itself.
5806+
// 'T : Enum
5807+
//
5808+
// In order to add this hack in a backwards-compatible way, we must hide this capability behind a marker type
5809+
// which we use solely as an indicator of whether the compiler understands `when 'T : Enum`.
5810+
// 'T : SupportsWhenTEnum
5811+
//
58045812
// canDecideTyparEqn is set to true in IlxGen when the witness-invoking implementation can be used.
58055813
let decideStaticOptimizationConstraint g c canDecideTyparEqn =
58065814
match c with
58075815
| TTyconEqualsTycon (a, b) when canDecideTyparEqn && typeEquiv g a b && isTyparTy g a ->
5808-
StaticOptimizationAnswer.Yes
5816+
StaticOptimizationAnswer.Yes
5817+
| TTyconEqualsTycon (_, b) when tryTcrefOfAppTy g b |> ValueOption.exists (tyconRefEq g g.SupportsWhenTEnum_tcr) ->
5818+
StaticOptimizationAnswer.Yes
5819+
| TTyconEqualsTycon (a, b) when isEnumTy g a && not (typeEquiv g a g.system_Enum_ty) && typeEquiv g b g.system_Enum_ty ->
5820+
StaticOptimizationAnswer.Yes
58095821
| TTyconEqualsTycon (a, b) ->
58105822
// Both types must be nominal for a definite result
58115823
let rec checkTypes a b =
@@ -5815,7 +5827,7 @@ let decideStaticOptimizationConstraint g c canDecideTyparEqn =
58155827
let b = normalizeEnumTy g (stripTyEqnsAndMeasureEqns g b)
58165828
match b with
58175829
| AppTy g (tcref2, _) ->
5818-
if tyconRefEq g tcref1 tcref2 then StaticOptimizationAnswer.Yes else StaticOptimizationAnswer.No
5830+
if tyconRefEq g tcref1 tcref2 && not (typeEquiv g a g.system_Enum_ty) then StaticOptimizationAnswer.Yes else StaticOptimizationAnswer.No
58195831
| RefTupleTy g _ | FunTy g _ -> StaticOptimizationAnswer.No
58205832
| _ -> StaticOptimizationAnswer.Unknown
58215833

src/FSharp.Core/prim-types.fs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,20 @@ namespace Microsoft.FSharp.Core
391391
type TailCallAttribute() =
392392
inherit System.Attribute()
393393

394+
namespace Microsoft.FSharp.Core.CompilerServices
395+
396+
open System.ComponentModel
397+
open Microsoft.FSharp.Core
398+
399+
/// <summary>
400+
/// A marker type that only compilers that support the <c>when 'T : Enum</c>
401+
/// library-only static optimization constraint will recognize.
402+
/// </summary>
403+
[<Sealed; AbstractClass>]
404+
[<EditorBrowsable(EditorBrowsableState.Never)>]
405+
[<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden = true)>]
406+
type SupportsWhenTEnum = class end
407+
394408
#if !NET5_0_OR_GREATER
395409
namespace System.Diagnostics.CodeAnalysis
396410

@@ -5149,11 +5163,10 @@ namespace Microsoft.FSharp.Core
51495163
when ^T : decimal = (# "conv.i" (int64 (# "" value : decimal #)) : unativeint #)
51505164
when ^T : ^T = (^T : (static member op_Explicit: ^T -> nativeint) (value))
51515165

5152-
[<CompiledName("ToString")>]
5153-
let inline string (value: 'T) =
5154-
anyToString "" value
5166+
let inline defaultString (value : 'T) =
5167+
anyToString "" value
51555168

5156-
when 'T : string =
5169+
when 'T : string =
51575170
if value = unsafeDefault<'T> then ""
51585171
else (# "" value : string #) // force no-op
51595172

@@ -5170,10 +5183,9 @@ namespace Microsoft.FSharp.Core
51705183
when 'T : nativeint = let x = (# "" value : nativeint #) in x.ToString()
51715184
when 'T : unativeint = let x = (# "" value : unativeint #) in x.ToString()
51725185

5173-
// Integral types can be enum:
5174-
// It is not possible to distinguish statically between Enum and (any type of) int. For signed types we have
5175-
// to use IFormattable::ToString, as the minus sign can be overridden. Using boxing we'll print their symbolic
5176-
// value if it's an enum, e.g.: 'ConsoleKey.Backspace' gives "Backspace", rather than "8")
5186+
// These rules for signed integer types will no longer be used when built with a compiler version that
5187+
// supports `when 'T : Enum`, but we must keep them to remain compatible with compiler versions that do not.
5188+
// Once all compiler versions that do not understand `when 'T : Enum` are out of support, these four rules can be removed.
51775189
when 'T : sbyte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
51785190
when 'T : int16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
51795191
when 'T : int32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
@@ -5186,7 +5198,6 @@ namespace Microsoft.FSharp.Core
51865198
when 'T : uint32 = let x = (# "" value : 'T #) in x.ToString()
51875199
when 'T : uint64 = let x = (# "" value : 'T #) in x.ToString()
51885200

5189-
51905201
// other common mscorlib System struct types
51915202
when 'T : DateTime = let x = (# "" value : DateTime #) in x.ToString(null, CultureInfo.InvariantCulture)
51925203
when 'T : DateTimeOffset = let x = (# "" value : DateTimeOffset #) in x.ToString(null, CultureInfo.InvariantCulture)
@@ -5206,6 +5217,38 @@ namespace Microsoft.FSharp.Core
52065217
if value = unsafeDefault<'T> then ""
52075218
else let x = (# "" value : IFormattable #) in defaultIfNull "" (x.ToString(null, CultureInfo.InvariantCulture))
52085219

5220+
[<CompiledName("ToString")>]
5221+
let inline string (value: 'T) =
5222+
defaultString value
5223+
5224+
// Only compilers that understand `when 'T : SupportsWhenTEnum` will understand `when 'T : Enum`.
5225+
when 'T : CompilerServices.SupportsWhenTEnum =
5226+
(
5227+
let inline string (value : 'T) =
5228+
defaultString value
5229+
5230+
// Special handling is required for enums, since:
5231+
//
5232+
// - The runtime value may be outside the defined members of the enum.
5233+
// - Their underlying type may be a signed integral type.
5234+
// - The negative sign may be overridden.
5235+
//
5236+
// For example:
5237+
//
5238+
// string DayOfWeek.Wednesday → "Wednesday"
5239+
// string (enum<DayOfWeek> -3) → "-3" // The negative sign is culture-dependent.
5240+
// string (enum<DayOfWeek> -3) → "⁒3" // E.g., the negative sign for the current culture could be overridden to "⁒".
5241+
when 'T : Enum = let x = (# "" value : 'T #) in x.ToString() // Use 'T to constrain the call to the specific enum type.
5242+
5243+
// For compilers that understand `when 'T : Enum`, we can safely make a constrained call on the integral type itself here.
5244+
when 'T : sbyte = let x = (# "" value : sbyte #) in x.ToString(null, CultureInfo.InvariantCulture)
5245+
when 'T : int16 = let x = (# "" value : int16 #) in x.ToString(null, CultureInfo.InvariantCulture)
5246+
when 'T : int32 = let x = (# "" value : int32 #) in x.ToString(null, CultureInfo.InvariantCulture)
5247+
when 'T : int64 = let x = (# "" value : int64 #) in x.ToString(null, CultureInfo.InvariantCulture)
5248+
5249+
string value
5250+
)
5251+
52095252
[<NoDynamicInvocation(isLegacy=true)>]
52105253
[<CompiledName("ToChar")>]
52115254
let inline char (value: ^T) =

src/FSharp.Core/prim-types.fsi

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,20 @@ namespace Microsoft.FSharp.Core
994994
inherit System.Attribute
995995
new : unit -> TailCallAttribute
996996

997+
namespace Microsoft.FSharp.Core.CompilerServices
998+
999+
open System.ComponentModel
1000+
open Microsoft.FSharp.Core
1001+
1002+
/// <summary>
1003+
/// A marker type that only compilers that support the <c>when 'T : Enum</c>
1004+
/// library-only static optimization constraint will recognize.
1005+
/// </summary>
1006+
[<Sealed; AbstractClass>]
1007+
[<EditorBrowsable(EditorBrowsableState.Never)>]
1008+
[<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden = true)>]
1009+
type SupportsWhenTEnum = class end
1010+
9971011
namespace System.Diagnostics.CodeAnalysis
9981012

9991013
open System
@@ -4772,7 +4786,7 @@ namespace Microsoft.FSharp.Core
47724786

47734787
/// <summary>Converts the argument to a string using <c>ToString</c>.</summary>
47744788
///
4775-
/// <remarks>For standard integer and floating point values and any type that implements <c>IFormattable</c>
4789+
/// <remarks>For standard integer and floating point values and any type that implements <c>IFormattable</c>,
47764790
/// <c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
47774791
/// <param name="value">The input value.</param>
47784792
///

tests/AheadOfTime/Trimming/check.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) {
4646
CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outputfile "FSharp.Core.dll" -expected_len 300032
4747

4848
# Check net8.0 trimmed assemblies
49-
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9150976
49+
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9154048

tests/FSharp.Compiler.ComponentTests/EmittedIL/Nullness/ReferenceDU.fs.il.net472.bsl

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,9 @@
662662
class MyTestModule/MyDu/JustInt V_2,
663663
int32 V_3,
664664
int32 V_4,
665-
class MyTestModule/MyDu/MaybeString V_5,
666-
string V_6)
665+
int32 V_5,
666+
class MyTestModule/MyDu/MaybeString V_6,
667+
string V_7)
667668
IL_0000: ldarg.0
668669
IL_0001: stloc.0
669670
IL_0002: ldloc.0
@@ -677,7 +678,7 @@
677678

678679
IL_000f: ldloc.1
679680
IL_0010: isinst MyTestModule/MyDu/MaybeString
680-
IL_0015: brtrue.s IL_0048
681+
IL_0015: brtrue.s IL_0040
681682

682683
IL_0017: br.s IL_001b
683684

@@ -696,23 +697,22 @@
696697
IL_002b: ldloc.3
697698
IL_002c: stloc.s V_4
698699
IL_002e: ldloc.s V_4
699-
IL_0030: box [runtime]System.Int32
700-
IL_0035: unbox.any [runtime]System.IFormattable
701-
IL_003a: ldnull
702-
IL_003b: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
703-
IL_0040: tail.
704-
IL_0042: callvirt instance string [netstandard]System.IFormattable::ToString(string,
705-
class [netstandard]System.IFormatProvider)
706-
IL_0047: ret
707-
708-
IL_0048: ldloc.0
709-
IL_0049: castclass MyTestModule/MyDu/MaybeString
710-
IL_004e: stloc.s V_5
711-
IL_0050: ldloc.s V_5
712-
IL_0052: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
713-
IL_0057: stloc.s V_6
714-
IL_0059: ldloc.s V_6
715-
IL_005b: ret
700+
IL_0030: stloc.s V_5
701+
IL_0032: ldloca.s V_5
702+
IL_0034: ldnull
703+
IL_0035: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
704+
IL_003a: call instance string [netstandard]System.Int32::ToString(string,
705+
class [netstandard]System.IFormatProvider)
706+
IL_003f: ret
707+
708+
IL_0040: ldloc.0
709+
IL_0041: castclass MyTestModule/MyDu/MaybeString
710+
IL_0046: stloc.s V_6
711+
IL_0048: ldloc.s V_6
712+
IL_004a: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
713+
IL_004f: stloc.s V_7
714+
IL_0051: ldloc.s V_7
715+
IL_0053: ret
716716
}
717717

718718
}

tests/FSharp.Compiler.ComponentTests/EmittedIL/Nullness/ReferenceDU.fs.il.netcore.bsl

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,9 @@
662662
class MyTestModule/MyDu/JustInt V_2,
663663
int32 V_3,
664664
int32 V_4,
665-
class MyTestModule/MyDu/MaybeString V_5,
666-
string V_6)
665+
int32 V_5,
666+
class MyTestModule/MyDu/MaybeString V_6,
667+
string V_7)
667668
IL_0000: ldarg.0
668669
IL_0001: stloc.0
669670
IL_0002: ldloc.0
@@ -677,7 +678,7 @@
677678

678679
IL_000f: ldloc.1
679680
IL_0010: isinst MyTestModule/MyDu/MaybeString
680-
IL_0015: brtrue.s IL_0048
681+
IL_0015: brtrue.s IL_0040
681682

682683
IL_0017: br.s IL_001b
683684

@@ -696,23 +697,22 @@
696697
IL_002b: ldloc.3
697698
IL_002c: stloc.s V_4
698699
IL_002e: ldloc.s V_4
699-
IL_0030: box [runtime]System.Int32
700-
IL_0035: unbox.any [runtime]System.IFormattable
701-
IL_003a: ldnull
702-
IL_003b: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
703-
IL_0040: tail.
704-
IL_0042: callvirt instance string [netstandard]System.IFormattable::ToString(string,
705-
class [netstandard]System.IFormatProvider)
706-
IL_0047: ret
707-
708-
IL_0048: ldloc.0
709-
IL_0049: castclass MyTestModule/MyDu/MaybeString
710-
IL_004e: stloc.s V_5
711-
IL_0050: ldloc.s V_5
712-
IL_0052: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
713-
IL_0057: stloc.s V_6
714-
IL_0059: ldloc.s V_6
715-
IL_005b: ret
700+
IL_0030: stloc.s V_5
701+
IL_0032: ldloca.s V_5
702+
IL_0034: ldnull
703+
IL_0035: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
704+
IL_003a: call instance string [netstandard]System.Int32::ToString(string,
705+
class [netstandard]System.IFormatProvider)
706+
IL_003f: ret
707+
708+
IL_0040: ldloc.0
709+
IL_0041: castclass MyTestModule/MyDu/MaybeString
710+
IL_0046: stloc.s V_6
711+
IL_0048: ldloc.s V_6
712+
IL_004a: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
713+
IL_004f: stloc.s V_7
714+
IL_0051: ldloc.s V_7
715+
IL_0053: ret
716716
}
717717

718718
}

0 commit comments

Comments
 (0)