Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit 64ff06e

Browse files
abelbraaksmanosami
authored andcommitted
Optimize 'string', remove boxing where possible, prevent FS0670, simplify generic code (RFC-1089) (dotnet#9549)
* Merge from Re-enable-tests-for-operators * Add tests that cover the changes for fixing FS0670 * Un-inline `string` to allow used in generic overrides and types * Fix 3x C# default impl tests by removing dep. on FSharp.Core.dll * Implement new 'string' ideas from RFC-1089 * Some housekeeping, adding IFormattable to the list * Further optimizations for unsigned ints and (u)nativeint * Adding back 'inline' but not SRTP * Ignore NCrunch temp files and local user files * Fix string of enum-of-char * Fix tests in ExprTests.fs * Distinguish between FSharp.Core 4.5, 4.6 and 4.7 in tests in ExprTests.fs
1 parent d93c98e commit 64ff06e

File tree

5 files changed

+1631
-1538
lines changed

5 files changed

+1631
-1538
lines changed

src/fsharp/FSharp.Core/prim-types.fs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -784,9 +784,9 @@ namespace Microsoft.FSharp.Core
784784

785785
let inline anyToString nullStr x =
786786
match box x with
787+
| :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture)
787788
| null -> nullStr
788-
| :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture)
789-
| obj -> obj.ToString()
789+
| _ -> x.ToString()
790790

791791
let anyToStringShowingNull x = anyToString "null" x
792792

@@ -3749,6 +3749,8 @@ namespace Microsoft.FSharp.Core
37493749
open System.Diagnostics
37503750
open System.Collections.Generic
37513751
open System.Globalization
3752+
open System.Text
3753+
open System.Numerics
37523754
open Microsoft.FSharp.Core
37533755
open Microsoft.FSharp.Core.LanguagePrimitives
37543756
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
@@ -4456,23 +4458,53 @@ namespace Microsoft.FSharp.Core
44564458
when ^T : ^T = (^T : (static member op_Explicit: ^T -> nativeint) (value))
44574459

44584460
[<CompiledName("ToString")>]
4459-
let inline string (value: ^T) =
4461+
let inline string (value: 'T) =
44604462
anyToString "" value
4461-
// since we have static optimization conditionals for ints below, we need to special-case Enums.
4462-
// This way we'll print their symbolic value, as opposed to their integral one (Eg., "A", rather than "1")
4463-
when ^T struct = anyToString "" value
4464-
when ^T : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture)
4465-
when ^T : float32 = (# "" value : float32 #).ToString("g",CultureInfo.InvariantCulture)
4466-
when ^T : int64 = (# "" value : int64 #).ToString("g",CultureInfo.InvariantCulture)
4467-
when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture)
4468-
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
4469-
when ^T : nativeint = (# "" value : nativeint #).ToString()
4470-
when ^T : sbyte = (# "" value : sbyte #).ToString("g",CultureInfo.InvariantCulture)
4471-
when ^T : uint64 = (# "" value : uint64 #).ToString("g",CultureInfo.InvariantCulture)
4472-
when ^T : uint32 = (# "" value : uint32 #).ToString("g",CultureInfo.InvariantCulture)
4473-
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
4474-
when ^T : unativeint = (# "" value : unativeint #).ToString()
4475-
when ^T : byte = (# "" value : byte #).ToString("g",CultureInfo.InvariantCulture)
4463+
when 'T : string = (# "" value : string #) // force no-op
4464+
4465+
// Using 'let x = (# ... #) in x.ToString()' leads to better IL, without it, an extra stloc and ldloca.s (get address-of)
4466+
// gets emitted, which are unnecessary. With it, the extra address-of-variable is not created
4467+
when 'T : float = let x = (# "" value : float #) in x.ToString(null, CultureInfo.InvariantCulture)
4468+
when 'T : float32 = let x = (# "" value : float32 #) in x.ToString(null, CultureInfo.InvariantCulture)
4469+
when 'T : decimal = let x = (# "" value : decimal #) in x.ToString(null, CultureInfo.InvariantCulture)
4470+
when 'T : BigInteger = let x = (# "" value : BigInteger #) in x.ToString(null, CultureInfo.InvariantCulture)
4471+
4472+
// no IFormattable
4473+
when 'T : char = let x = (# "" value : 'T #) in x.ToString() // use 'T, because char can be an enum
4474+
when 'T : bool = let x = (# "" value : bool #) in x.ToString()
4475+
when 'T : nativeint = let x = (# "" value : nativeint #) in x.ToString()
4476+
when 'T : unativeint = let x = (# "" value : unativeint #) in x.ToString()
4477+
4478+
// Integral types can be enum:
4479+
// It is not possible to distinguish statically between Enum and (any type of) int. For signed types we have
4480+
// to use IFormattable::ToString, as the minus sign can be overridden. Using boxing we'll print their symbolic
4481+
// value if it's an enum, e.g.: 'ConsoleKey.Backspace' gives "Backspace", rather than "8")
4482+
when 'T : sbyte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
4483+
when 'T : int16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
4484+
when 'T : int32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
4485+
when 'T : int64 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
4486+
4487+
// unsigned integral types have equal behavior with 'T::ToString() vs IFormattable::ToString
4488+
// this allows us to issue the 'constrained' opcode with 'callvirt'
4489+
when 'T : byte = let x = (# "" value : 'T #) in x.ToString()
4490+
when 'T : uint16 = let x = (# "" value : 'T #) in x.ToString()
4491+
when 'T : uint32 = let x = (# "" value : 'T #) in x.ToString()
4492+
when 'T : uint64 = let x = (# "" value : 'T #) in x.ToString()
4493+
4494+
4495+
// other common mscorlib System struct types
4496+
when 'T : DateTime = let x = (# "" value : DateTime #) in x.ToString(null, CultureInfo.InvariantCulture)
4497+
when 'T : DateTimeOffset = let x = (# "" value : DateTimeOffset #) in x.ToString(null, CultureInfo.InvariantCulture)
4498+
when 'T : TimeSpan = let x = (# "" value : TimeSpan #) in x.ToString(null, CultureInfo.InvariantCulture)
4499+
when 'T : Guid = let x = (# "" value : Guid #) in x.ToString(null, CultureInfo.InvariantCulture)
4500+
when 'T struct =
4501+
match box value with
4502+
| :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture)
4503+
| _ -> value.ToString()
4504+
4505+
// other commmon mscorlib reference types
4506+
when 'T : StringBuilder = let x = (# "" value : StringBuilder #) in x.ToString()
4507+
when 'T : IFormattable = let x = (# "" value : IFormattable #) in x.ToString(null, CultureInfo.InvariantCulture)
44764508

44774509
[<NoDynamicInvocation(isLegacy=true)>]
44784510
[<CompiledName("ToChar")>]

src/fsharp/FSharp.Core/prim-types.fsi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3407,13 +3407,13 @@ namespace Microsoft.FSharp.Core
34073407

34083408
/// <summary>Converts the argument to a string using <c>ToString</c>.</summary>
34093409
///
3410-
/// <remarks>For standard integer and floating point values the <c>ToString</c> conversion
3411-
/// uses <c>CultureInfo.InvariantCulture</c>. </remarks>
3410+
/// <remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c>
3411+
/// <c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
34123412
/// <param name="value">The input value.</param>
34133413
///
34143414
/// <returns>The converted string.</returns>
34153415
[<CompiledName("ToString")>]
3416-
val inline string : value:^T -> string
3416+
val inline string : value:'T -> string
34173417

34183418
/// <summary>Converts the argument to System.Decimal using a direct conversion for all
34193419
/// primitive numeric types. For strings, the input is converted using <c>UInt64.Parse()</c>

tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ namespace FSharp.Core.UnitTests.Operators
88
open System
99
open FSharp.Core.UnitTests.LibraryTestFx
1010
open NUnit.Framework
11+
open System.Globalization
12+
open System.Threading
13+
14+
/// If this type compiles without error it is correct
15+
/// Wrong if you see: FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope.
16+
type TestFs0670Error<'T> =
17+
| TestFs0670Error of 'T
18+
override this.ToString() =
19+
match this with
20+
| TestFs0670Error x ->
21+
// This used to raise FS0670 because the type is generic, and 'string' was inline
22+
// See: https://github.com/dotnet/fsharp/issues/7958
23+
Operators.string x
1124

1225
[<TestFixture>]
1326
type OperatorsModule2() =
@@ -732,6 +745,53 @@ type OperatorsModule2() =
732745
// reference type
733746
let result = Operators.string "ABC"
734747
Assert.AreEqual("ABC", result)
748+
749+
// reference type without a `ToString()` overload
750+
let result = Operators.string (obj())
751+
Assert.AreEqual("System.Object", result)
752+
753+
let result = Operators.string 1un
754+
Assert.AreEqual("1", result)
755+
756+
let result = Operators.string (obj())
757+
Assert.AreEqual("System.Object", result)
758+
759+
let result = Operators.string 123.456M
760+
Assert.AreEqual("123.456", result)
761+
762+
// Following tests ensure that InvariantCulture is used if type implements IFormattable
763+
764+
// safe current culture, then switch culture
765+
let currentCI = Thread.CurrentThread.CurrentCulture
766+
Thread.CurrentThread.CurrentCulture <- CultureInfo.GetCultureInfo("de-DE")
767+
768+
// make sure the culture switch happened, and verify
769+
let wrongResult = 123.456M.ToString()
770+
Assert.AreEqual("123,456", wrongResult)
771+
772+
// test that culture has no influence on decimals with `string`
773+
let correctResult = Operators.string 123.456M
774+
Assert.AreEqual("123.456", correctResult)
775+
776+
// make sure that the German culture is indeed selected for DateTime
777+
let dttm = DateTime(2020, 6, 23)
778+
let wrongResult = dttm.ToString()
779+
Assert.AreEqual("23.06.2020 00:00:00", wrongResult)
780+
781+
// test that culture has no influence on DateTime types when used with `string`
782+
let correctResult = Operators.string dttm
783+
Assert.AreEqual("06/23/2020 00:00:00", correctResult)
784+
785+
// reset the culture
786+
Thread.CurrentThread.CurrentCulture <- currentCI
787+
788+
[<Test>]
789+
member _.``string: don't raise FS0670 anymore``() =
790+
// The type used here, when compiled, should not raise this error:
791+
// "FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope."
792+
// See: https://github.com/dotnet/fsharp/issues/7958
793+
let result = TestFs0670Error 32uy |> Operators.string
794+
Assert.AreEqual("32", result)
735795

736796
[<Test>]
737797
member _.tan() =

tests/fsharp/Compiler/Language/DefaultInterfaceMemberTests.fs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -810,15 +810,15 @@ type Test () =
810810
member __.M(_x: int) = Console.Write("InTest")
811811
812812
member __.M<'Item> (x: int, y: 'Item) =
813-
Console.Write(string x)
813+
Console.Write(x.ToString())
814814
Console.Write(y.ToString ())
815815
816816
member __.M<'TTT> (x: 'TTT) =
817817
Console.Write(x.ToString ())
818818
819819
member __.M (x: int, text: string) =
820820
Console.Write("ABC")
821-
Console.Write(string x)
821+
Console.Write(x.ToString())
822822
Console.Write(text)
823823
824824
member __.M<'U> (_x: 'U, _y: int) = ()
@@ -1166,7 +1166,8 @@ type Test () =
11661166
let main _ =
11671167
let x = Test () :> I1
11681168
let y = Test () :> I2
1169-
Console.Write(string (x + y))
1169+
let result = x + y
1170+
Console.Write(result.ToString())
11701171
0
11711172
"""
11721173

@@ -4229,15 +4230,15 @@ type Test () =
42294230
member __.M(_x: int) = Console.Write("InTest")
42304231
42314232
member __.M<'Item> (x: int, y: 'Item) =
4232-
Console.Write(string x)
4233+
Console.Write(x.ToString())
42334234
Console.Write(y.ToString ())
42344235
42354236
member __.M<'TTT> (x: 'TTT) =
42364237
Console.Write(x.ToString ())
42374238
42384239
member __.M (x: int, text: string) =
42394240
Console.Write("ABC")
4240-
Console.Write(string x)
4241+
Console.Write(x.ToString())
42414242
Console.Write(text)
42424243
42434244
type Test2 () =

0 commit comments

Comments
 (0)