diff --git a/src/fsharp/FSharp.Core/prim-types.fs b/src/fsharp/FSharp.Core/prim-types.fs index d0d608f1190..d52ef1283e8 100644 --- a/src/fsharp/FSharp.Core/prim-types.fs +++ b/src/fsharp/FSharp.Core/prim-types.fs @@ -781,9 +781,9 @@ namespace Microsoft.FSharp.Core let inline anyToString nullStr x = match box x with + | :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture) | null -> nullStr - | :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture) - | obj -> obj.ToString() + | _ -> x.ToString() let anyToStringShowingNull x = anyToString "null" x @@ -3746,6 +3746,8 @@ namespace Microsoft.FSharp.Core open System.Diagnostics open System.Collections.Generic open System.Globalization + open System.Text + open System.Numerics open Microsoft.FSharp.Core open Microsoft.FSharp.Core.LanguagePrimitives open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators @@ -4457,23 +4459,52 @@ namespace Microsoft.FSharp.Core when ^T : ^T = (^T : (static member op_Explicit: ^T -> nativeint) (value)) [] - let inline string (value: ^T) = + let string (value: 'T) = anyToString "" value - // since we have static optimization conditionals for ints below, we need to special-case Enums. - // This way we'll print their symbolic value, as opposed to their integral one (Eg., "A", rather than "1") - when ^T struct = anyToString "" value - when ^T : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture) - when ^T : float32 = (# "" value : float32 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : int64 = (# "" value : int64 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : nativeint = (# "" value : nativeint #).ToString() - when ^T : sbyte = (# "" value : sbyte #).ToString("g",CultureInfo.InvariantCulture) - when ^T : uint64 = (# "" value : uint64 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : uint32 = (# "" value : uint32 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture) - when ^T : unativeint = (# "" value : unativeint #).ToString() - when ^T : byte = (# "" value : byte #).ToString("g",CultureInfo.InvariantCulture) + when 'T : string = (# "" value : string #) // force no-op + + // Using 'let x = (# ... #) in x.ToString()' leads to better IL, without it, an extra stloc and ldloca.s (get address-of) + // gets emitted, which are unnecessary. With it, the extra address-of-variable is not created + when 'T : float = let x = (# "" value : float #) in x.ToString(null, CultureInfo.InvariantCulture) + when 'T : float32 = let x = (# "" value : float32 #) in x.ToString(null, CultureInfo.InvariantCulture) + when 'T : decimal = let x = (# "" value : decimal #) in x.ToString(null, CultureInfo.InvariantCulture) + when 'T : BigInteger = let x = (# "" value : BigInteger #) in x.ToString(null, CultureInfo.InvariantCulture) + + // no IFormattable + when 'T : char = let x = (# "" value : char #) in x.ToString() + when 'T : bool = let x = (# "" value : bool #) in x.ToString() + + // For the int-types: + // It is not possible to distinguish statically between Enum and (any type of) int. + // This way we'll print their symbolic value, as opposed to their integral one + // E.g.: 'string ConsoleKey.Backspace' gives "Backspace", rather than "8") + when 'T : sbyte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : byte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : int16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : uint16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : int32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : uint32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : int64 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + when 'T : uint64 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture) + + // native ints cannot be used for enums, and do not implement IFormattable + when 'T : nativeint = (# "" value : nativeint #).ToString() + when 'T : unativeint = (# "" value : unativeint #).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) + when 'T : TimeSpan = let x = (# "" value : TimeSpan #) in x.ToString(null, CultureInfo.InvariantCulture) + when 'T : Guid = let x = (# "" value : Guid #) in x.ToString(null, CultureInfo.InvariantCulture) + when 'T struct = + match box value with + | :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture) + | _ -> value.ToString() + + // other commmon mscorlib reference types + when 'T : StringBuilder = let x = (# "" value : StringBuilder #) in x.ToString() + when 'T : IFormattable = let x = (# "" value : IFormattable #) in x.ToString(null, CultureInfo.InvariantCulture) + when 'T : option<_> = let x = (# "" value : option<_> #) in match x with None -> "None" | _ -> x.ToString() [] [] diff --git a/src/fsharp/FSharp.Core/prim-types.fsi b/src/fsharp/FSharp.Core/prim-types.fsi index 3c35192188e..45c57a544af 100644 --- a/src/fsharp/FSharp.Core/prim-types.fsi +++ b/src/fsharp/FSharp.Core/prim-types.fsi @@ -2824,12 +2824,12 @@ namespace Microsoft.FSharp.Core /// Converts the argument to a string using ToString. /// - /// For standard integer and floating point values the ToString conversion - /// uses CultureInfo.InvariantCulture. + /// For standard integer and floating point values the and any type that implements IFormattable + /// ToString conversion uses CultureInfo.InvariantCulture. /// The input value. /// The converted string. [] - val inline string : value:^T -> string + val string : value:'T -> string /// Converts the argument to System.Decimal using a direct conversion for all /// primitive numeric types. For strings, the input is converted using UInt64.Parse() diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs index da874ebe118..9fba8e9bbc1 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs @@ -8,6 +8,19 @@ namespace FSharp.Core.UnitTests.Operators open System open FSharp.Core.UnitTests.LibraryTestFx open NUnit.Framework +open System.Globalization +open System.Threading + +/// If this type compiles without error it is correct +/// 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. +type TestFs0670Error<'T> = + | TestFs0670Error of 'T + override this.ToString() = + match this with + | TestFs0670Error x -> + // This used to raise FS0670 because the type is generic, and 'string' was inline + // See: https://github.com/dotnet/fsharp/issues/7958 + Operators.string x [] type OperatorsModule2() = @@ -718,6 +731,53 @@ type OperatorsModule2() = // reference type let result = Operators.string "ABC" Assert.AreEqual("ABC", result) + + // reference type without a `ToString()` overload + let result = Operators.string (obj()) + Assert.AreEqual("System.Object", result) + + let result = Operators.string 1un + Assert.AreEqual("1", result) + + let result = Operators.string (obj()) + Assert.AreEqual("System.Object", result) + + let result = Operators.string 123.456M + Assert.AreEqual("123.456", result) + + // Following tests ensure that InvariantCulture is used if type implements IFormattable + + // safe current culture, then switch culture + let currentCI = Thread.CurrentThread.CurrentCulture + Thread.CurrentThread.CurrentCulture <- CultureInfo.GetCultureInfo("de-DE") + + // make sure the culture switch happened, and verify + let wrongResult = 123.456M.ToString() + Assert.AreEqual("123,456", wrongResult) + + // test that culture has no influence on decimals with `string` + let correctResult = Operators.string 123.456M + Assert.AreEqual("123.456", correctResult) + + // make sure that the German culture is indeed selected for DateTime + let dttm = DateTime(2020, 6, 23) + let wrongResult = dttm.ToString() + Assert.AreEqual("23.06.2020 00:00:00", wrongResult) + + // test that culture has no influence on DateTime types when used with `string` + let correctResult = Operators.string dttm + Assert.AreEqual("06/23/2020 00:00:00", correctResult) + + // reset the culture + Thread.CurrentThread.CurrentCulture <- currentCI + + [] + member _.``string: don't raise FS0670 anymore``() = + // The type used here, when compiled, should not raise this error: + // "FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope." + // See: https://github.com/dotnet/fsharp/issues/7958 + let result = TestFs0670Error 32uy |> Operators.string + Assert.AreEqual("32", result) [] member _.tan() = diff --git a/tests/fsharp/Compiler/Language/DefaultInterfaceMemberTests.fs b/tests/fsharp/Compiler/Language/DefaultInterfaceMemberTests.fs index d64067ddfa9..964f804a8a8 100644 --- a/tests/fsharp/Compiler/Language/DefaultInterfaceMemberTests.fs +++ b/tests/fsharp/Compiler/Language/DefaultInterfaceMemberTests.fs @@ -813,7 +813,7 @@ type Test () = member __.M(_x: int) = Console.Write("InTest") member __.M<'Item> (x: int, y: 'Item) = - Console.Write(string x) + Console.Write(x.ToString()) Console.Write(y.ToString ()) member __.M<'TTT> (x: 'TTT) = @@ -821,7 +821,7 @@ type Test () = member __.M (x: int, text: string) = Console.Write("ABC") - Console.Write(string x) + Console.Write(x.ToString()) Console.Write(text) member __.M<'U> (_x: 'U, _y: int) = () @@ -1166,7 +1166,8 @@ type Test () = let main _ = let x = Test () :> I1 let y = Test () :> I2 - Console.Write(string (x + y)) + let result = x + y + Console.Write(result.ToString()) 0 """ @@ -4229,7 +4230,7 @@ type Test () = member __.M(_x: int) = Console.Write("InTest") member __.M<'Item> (x: int, y: 'Item) = - Console.Write(string x) + Console.Write(x.ToString()) Console.Write(y.ToString ()) member __.M<'TTT> (x: 'TTT) = @@ -4237,7 +4238,7 @@ type Test () = member __.M (x: int, text: string) = Console.Write("ABC") - Console.Write(string x) + Console.Write(x.ToString()) Console.Write(text) type Test2 () =