Skip to content

More string optimizations #18546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Compiler.Service/10.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
* Allow `let!` and `use!` type annotations without requiring parentheses ([PR #18508](https://github.com/dotnet/fsharp/pull/18508))
* Fix find all references for F# exceptions ([PR #18565](https://github.com/dotnet/fsharp/pull/18565))
* Shorthand lambda: fix completion for chained calls and analysis for unfinished expression ([PR #18560](https://github.com/dotnet/fsharp/pull/18560))

### Added

* Add support for `when 'T : Enum` library-only library-only static optimization constraint. ([PR #18546](https://github.com/dotnet/fsharp/pull/18546))
4 changes: 3 additions & 1 deletion docs/release-notes/.FSharp.Core/10.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
### Changed

* Random functions support for zero element chosen/sampled ([PR #18568](https://github.com/dotnet/fsharp/pull/18568))
* 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))

### Breaking Changes

### Breaking Changes
2 changes: 2 additions & 0 deletions src/Compiler/TypedTree/TcGlobals.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,8 @@ type TcGlobals(

member val ArrayCollector_tcr = mk_MFCompilerServices_tcref fslibCcu "ArrayCollector`1"

member val SupportsWhenTEnum_tcr = mk_MFCompilerServices_tcref fslibCcu "SupportsWhenTEnum"

member _.TryEmbedILType(tref: ILTypeRef, mkEmbeddableType: unit -> ILTypeDef) =
if tref.Scope = ILScopeRef.Local && not(embeddedILTypeDefs.ContainsKey(tref.Name)) then
embeddedILTypeDefs.TryAdd(tref.Name, mkEmbeddableType()) |> ignore
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/TypedTree/TcGlobals.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ type internal TcGlobals =

member ListCollector_tcr: FSharp.Compiler.TypedTree.EntityRef

member SupportsWhenTEnum_tcr: FSharp.Compiler.TypedTree.EntityRef

member MatchFailureException_tcr: FSharp.Compiler.TypedTree.EntityRef

member ResumableCode_tcr: FSharp.Compiler.TypedTree.EntityRef
Expand Down
16 changes: 14 additions & 2 deletions src/Compiler/TypedTree/TypedTreeOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5801,11 +5801,23 @@ type StaticOptimizationAnswer =
// ^T : ^T --> used in (+), (-) etc. to guard witness-invoking implementations added in F# 5
// 'T : 'T --> used in FastGenericEqualityComparer, FastGenericComparer to guard struct/tuple implementations
//
// For performance and compatibility reasons, 'T when 'T is an enum is handled with its own special hack.
// Unlike for other 'T : tycon constraints, 'T can be any enum; it need not (and indeed must not) be identical to System.Enum itself.
// 'T : Enum
//
// In order to add this hack in a backwards-compatible way, we must hide this capability behind a marker type
// which we use solely as an indicator of whether the compiler understands `when 'T : Enum`.
// 'T : SupportsWhenTEnum
//
// canDecideTyparEqn is set to true in IlxGen when the witness-invoking implementation can be used.
let decideStaticOptimizationConstraint g c canDecideTyparEqn =
match c with
| TTyconEqualsTycon (a, b) when canDecideTyparEqn && typeEquiv g a b && isTyparTy g a ->
StaticOptimizationAnswer.Yes
StaticOptimizationAnswer.Yes
| TTyconEqualsTycon (_, b) when tryTcrefOfAppTy g b |> ValueOption.exists (tyconRefEq g g.SupportsWhenTEnum_tcr) ->
StaticOptimizationAnswer.Yes
| TTyconEqualsTycon (a, b) when isEnumTy g a && not (typeEquiv g a g.system_Enum_ty) && typeEquiv g b g.system_Enum_ty ->
StaticOptimizationAnswer.Yes
| TTyconEqualsTycon (a, b) ->
// Both types must be nominal for a definite result
let rec checkTypes a b =
Expand All @@ -5815,7 +5827,7 @@ let decideStaticOptimizationConstraint g c canDecideTyparEqn =
let b = normalizeEnumTy g (stripTyEqnsAndMeasureEqns g b)
match b with
| AppTy g (tcref2, _) ->
if tyconRefEq g tcref1 tcref2 then StaticOptimizationAnswer.Yes else StaticOptimizationAnswer.No
if tyconRefEq g tcref1 tcref2 && not (typeEquiv g a g.system_Enum_ty) then StaticOptimizationAnswer.Yes else StaticOptimizationAnswer.No
| RefTupleTy g _ | FunTy g _ -> StaticOptimizationAnswer.No
| _ -> StaticOptimizationAnswer.Unknown

Expand Down
61 changes: 52 additions & 9 deletions src/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,20 @@ namespace Microsoft.FSharp.Core
type TailCallAttribute() =
inherit System.Attribute()

namespace Microsoft.FSharp.Core.CompilerServices

open System.ComponentModel
open Microsoft.FSharp.Core

/// <summary>
/// A marker type that only compilers that support the <c>when 'T : Enum</c>
/// library-only static optimization constraint will recognize.
/// </summary>
[<Sealed; AbstractClass>]
[<EditorBrowsable(EditorBrowsableState.Never)>]
[<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden = true)>]
type SupportsWhenTEnum = class end

#if !NET5_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis

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

[<CompiledName("ToString")>]
let inline string (value: 'T) =
anyToString "" value
let inline defaultString (value : 'T) =
anyToString "" value

when 'T : string =
when 'T : string =
if value = unsafeDefault<'T> then ""
else (# "" value : string #) // force no-op

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

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


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

[<CompiledName("ToString")>]
let inline string (value: 'T) =
defaultString value

// Only compilers that understand `when 'T : SupportsWhenTEnum` will understand `when 'T : Enum`.
when 'T : CompilerServices.SupportsWhenTEnum =
(
let inline string (value : 'T) =
defaultString value

// Special handling is required for enums, since:
//
// - The runtime value may be outside the defined members of the enum.
// - Their underlying type may be a signed integral type.
// - The negative sign may be overridden.
//
// For example:
//
// string DayOfWeek.Wednesday → "Wednesday"
// string (enum<DayOfWeek> -3) → "-3" // The negative sign is culture-dependent.
// string (enum<DayOfWeek> -3) → "⁒3" // E.g., the negative sign for the current culture could be overridden to "⁒".
when 'T : Enum = let x = (# "" value : 'T #) in x.ToString() // Use 'T to constrain the call to the specific enum type.

// For compilers that understand `when 'T : Enum`, we can safely make a constrained call on the integral type itself here.
when 'T : sbyte = let x = (# "" value : sbyte #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : int16 = let x = (# "" value : int16 #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : int32 = let x = (# "" value : int32 #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : int64 = let x = (# "" value : int64 #) in x.ToString(null, CultureInfo.InvariantCulture)

string value
)

[<NoDynamicInvocation(isLegacy=true)>]
[<CompiledName("ToChar")>]
let inline char (value: ^T) =
Expand Down
16 changes: 15 additions & 1 deletion src/FSharp.Core/prim-types.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,20 @@ namespace Microsoft.FSharp.Core
inherit System.Attribute
new : unit -> TailCallAttribute

namespace Microsoft.FSharp.Core.CompilerServices

open System.ComponentModel
open Microsoft.FSharp.Core

/// <summary>
/// A marker type that only compilers that support the <c>when 'T : Enum</c>
/// library-only static optimization constraint will recognize.
/// </summary>
[<Sealed; AbstractClass>]
[<EditorBrowsable(EditorBrowsableState.Never)>]
[<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden = true)>]
type SupportsWhenTEnum = class end

namespace System.Diagnostics.CodeAnalysis

open System
Expand Down Expand Up @@ -4772,7 +4786,7 @@ namespace Microsoft.FSharp.Core

/// <summary>Converts the argument to a string using <c>ToString</c>.</summary>
///
/// <remarks>For standard integer and floating point values and any type that implements <c>IFormattable</c>
/// <remarks>For standard integer and floating point values and any type that implements <c>IFormattable</c>,
/// <c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
/// <param name="value">The input value.</param>
///
Expand Down
2 changes: 1 addition & 1 deletion tests/AheadOfTime/Trimming/check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) {
CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outputfile "FSharp.Core.dll" -expected_len 300032

# Check net8.0 trimmed assemblies
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9150976
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9154048
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,9 @@
class MyTestModule/MyDu/JustInt V_2,
int32 V_3,
int32 V_4,
class MyTestModule/MyDu/MaybeString V_5,
string V_6)
int32 V_5,
class MyTestModule/MyDu/MaybeString V_6,
string V_7)
IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
Expand All @@ -677,7 +678,7 @@

IL_000f: ldloc.1
IL_0010: isinst MyTestModule/MyDu/MaybeString
IL_0015: brtrue.s IL_0048
IL_0015: brtrue.s IL_0040

IL_0017: br.s IL_001b

Expand All @@ -696,23 +697,22 @@
IL_002b: ldloc.3
IL_002c: stloc.s V_4
IL_002e: ldloc.s V_4
IL_0030: box [runtime]System.Int32
IL_0035: unbox.any [runtime]System.IFormattable
IL_003a: ldnull
IL_003b: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
IL_0040: tail.
IL_0042: callvirt instance string [netstandard]System.IFormattable::ToString(string,
class [netstandard]System.IFormatProvider)
IL_0047: ret

IL_0048: ldloc.0
IL_0049: castclass MyTestModule/MyDu/MaybeString
IL_004e: stloc.s V_5
IL_0050: ldloc.s V_5
IL_0052: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
IL_0057: stloc.s V_6
IL_0059: ldloc.s V_6
IL_005b: ret
IL_0030: stloc.s V_5
IL_0032: ldloca.s V_5
IL_0034: ldnull
IL_0035: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
IL_003a: call instance string [netstandard]System.Int32::ToString(string,
class [netstandard]System.IFormatProvider)
IL_003f: ret

IL_0040: ldloc.0
IL_0041: castclass MyTestModule/MyDu/MaybeString
IL_0046: stloc.s V_6
IL_0048: ldloc.s V_6
IL_004a: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
IL_004f: stloc.s V_7
IL_0051: ldloc.s V_7
IL_0053: ret
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,9 @@
class MyTestModule/MyDu/JustInt V_2,
int32 V_3,
int32 V_4,
class MyTestModule/MyDu/MaybeString V_5,
string V_6)
int32 V_5,
class MyTestModule/MyDu/MaybeString V_6,
string V_7)
IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
Expand All @@ -677,7 +678,7 @@

IL_000f: ldloc.1
IL_0010: isinst MyTestModule/MyDu/MaybeString
IL_0015: brtrue.s IL_0048
IL_0015: brtrue.s IL_0040

IL_0017: br.s IL_001b

Expand All @@ -696,23 +697,22 @@
IL_002b: ldloc.3
IL_002c: stloc.s V_4
IL_002e: ldloc.s V_4
IL_0030: box [runtime]System.Int32
IL_0035: unbox.any [runtime]System.IFormattable
IL_003a: ldnull
IL_003b: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
IL_0040: tail.
IL_0042: callvirt instance string [netstandard]System.IFormattable::ToString(string,
class [netstandard]System.IFormatProvider)
IL_0047: ret

IL_0048: ldloc.0
IL_0049: castclass MyTestModule/MyDu/MaybeString
IL_004e: stloc.s V_5
IL_0050: ldloc.s V_5
IL_0052: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
IL_0057: stloc.s V_6
IL_0059: ldloc.s V_6
IL_005b: ret
IL_0030: stloc.s V_5
IL_0032: ldloca.s V_5
IL_0034: ldnull
IL_0035: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()
IL_003a: call instance string [netstandard]System.Int32::ToString(string,
class [netstandard]System.IFormatProvider)
IL_003f: ret

IL_0040: ldloc.0
IL_0041: castclass MyTestModule/MyDu/MaybeString
IL_0046: stloc.s V_6
IL_0048: ldloc.s V_6
IL_004a: ldfld string MyTestModule/MyDu/MaybeString::_nullableString
IL_004f: stloc.s V_7
IL_0051: ldloc.s V_7
IL_0053: ret
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace EmittedIL

open FSharp.Test
open FSharp.Test.Compiler
open Xunit

module StaticOptimizations =
let verifyCompilation compilation =
compilation
|> asExe
|> withEmbeddedPdb
|> withEmbedAllSource
|> ignoreWarnings
|> verifyILBaseline

[<Theory; FileInlineData("String_Enum.fs", Realsig = BooleanOptions.True, Optimize = BooleanOptions.True)>]
let String_Enum_fs compilation =
compilation
|> getCompilation
|> verifyCompilation

[<Theory; FileInlineData("String_SignedIntegralTypes.fs", Realsig = BooleanOptions.True, Optimize = BooleanOptions.True)>]
let String_SignedIntegralTypes_fs compilation =
compilation
|> getCompilation
|> verifyCompilation
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
open System

module String =
type CharEnum = Char = 'a'
type SByteEnum = SByte = 1y
type Int16Enum = Int16 = 1s
type Int32Enum = Int32 = 1
type Int64Enum = Int64 = 1L

type ByteEnum = Byte = 1uy
type UInt16Enum = UInt16 = 1us
type UInt32Enum = UInt32 = 1u
type UInt64Enum = UInt64 = 1UL

let ``string<CharEnum>`` (enum : CharEnum) = string enum
let ``string<SByteEnum>`` (enum : SByteEnum) = string enum
let ``string<Int16Enum>`` (enum : Int16Enum) = string enum
let ``string<Int32Enum>`` (enum : Int32Enum) = string enum
let ``string<Int64Enum>`` (enum : Int64Enum) = string enum

let ``string<ByteEnum>`` (enum : ByteEnum) = string enum
let ``string<UInt16Enum>`` (enum : UInt16Enum) = string enum
let ``string<UInt32Enum>`` (enum : UInt32Enum) = string enum
let ``string<UInt64Enum>`` (enum : UInt64Enum) = string enum

let ``string<#Enum>`` (enum : #Enum) = string enum
let ``string<'T :> Enum>`` (enum : 'T :> Enum) = string enum

let ``string<'T when 'T : enum<'U>>`` (enum : 'T when 'T : enum<'U>) = string enum
let ``string<'T when 'T : enum<int>>`` (enum : 'T when 'T : enum<int>) = string enum

let ``string Unchecked.defaultof<System.Enum>`` () = string Unchecked.defaultof<System.Enum>
Loading
Loading