dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.9k stars 783 forks source link

dict has invalid enumerator on dotnet core #5910

Closed KevinRansom closed 5 years ago

KevinRansom commented 5 years ago

This code behaves differently on NetCore and NetFramework

What happens is when MoveNext has moved past the end of the collection it's Enumerator needs to throw invalid operation exception on Current, instead it grabs the last value

// Learn more about F# at http://fsharp.org

open System
open System.Collections.Generic

[<EntryPoint>]
let main argv =

    let ie = (dict [|(1,1);(2,4);(3,9)|]) :> IEnumerable<KeyValuePair<_,_>>
    let enum = ie.GetEnumerator()

    let step2 = enum.MoveNext()
    let step3 = enum.Current
    printfn "%b %A" step2 step3   |> ignore

    let step4 = enum.MoveNext()
    let step5 = enum.Current
    printfn "%b %A" step4 step5   |> ignore

    let step6 = enum.MoveNext()
    let step7 = enum.Current
    printfn "%b %A" step6 step7   |> ignore

    let step8 = enum.MoveNext()
    let step9 = enum.Current
    printfn "%b %A" step8 step9   |> ignore

    let step10 = enum.MoveNext()
    let step11 = enum.Current

    printfn "%b %A" step10 step11 |> ignore

    0 // return an integer exit code
Output on NetCore:
C:\Users\kevinr\source\repos\ConsoleApp22\ConsoleApp22\bin\Debug>dotnet C:\Users\kevinr\source\repos\ConsoleApp22\ConsoleApp22\bin\Debug\netcoreapp2.1\ConsoleApp22.dll
true [1, 1]
true [2, 4]
true [3, 9]
false [3, 9]
false [3, 9]

Output on NetFramework
C:\Users\kevinr\source\repos\ConsoleApp22\ConsoleApp22\bin\Debug>C:\Users\kevinr\source\repos\ConsoleApp22\ConsoleApp22\bin\Debug\net472\ConsoleApp22.exe
true [1, 1]
true [2, 4]
true [3, 9]

Unhandled Exception: System.InvalidOperationException: Enumeration already finished.
   at System.SZArrayHelper.SZGenericArrayEnumerator`1.get_Current()
   at Program.main(String[] argv) in C:\Users\kevinr\source\repos\ConsoleApp22\ConsoleApp22\Program.fs:line 25
cartermp commented 5 years ago

There is a difference when cast as a generic IEnumerable vs non-generic IEnumerable.

Generic: https://github.com/Microsoft/visualfsharp/blob/fead0aac540485683f694524eadad79983ec28d9/src/fsharp/FSharp.Core/fslib-extra-pervasives.fs#L113

Non-generic: https://github.com/Microsoft/visualfsharp/blob/fead0aac540485683f694524eadad79983ec28d9/src/fsharp/FSharp.Core/fslib-extra-pervasives.fs#L120

If you change the interface cast to System.Collections.IEnumerable (non-generic), it behaves as you'd expect:

open System
open System.Collections
open System.Collections.Generic

[<EntryPoint>]
let main argv =
    let ie = (dict [|(1,1);(2,4);(3,9)|]) :> IEnumerable//<KeyValuePair<_,_>>
    let enum = ie.GetEnumerator()

    let step2 = enum.MoveNext()
    let step3 = enum.Current
    printfn "%b %A" step2 step3   |> ignore

    let step4 = enum.MoveNext()
    let step5 = enum.Current
    printfn "%b %A" step4 step5   |> ignore

    let step6 = enum.MoveNext()
    let step7 = enum.Current
    printfn "%b %A" step6 step7   |> ignore

    let step8 = enum.MoveNext()
    let step9 = enum.Current
    printfn "%b %A" step8 step9   |> ignore

    let step10 = enum.MoveNext()
    let step11 = enum.Current

    printfn "%b %A" step10 step11 |> ignore

image

Somehow, using seq<_> is what causes the difference on .NET Core.

KevinRansom commented 5 years ago

@cartermp yes, it is indeed the generic that is broken.

KevinRansom commented 5 years ago

This is a coreclr bug, issued raised with them here: https://github.com/dotnet/core/issues/2064

Here is the C# repro, if you are interested:

namespace ConsoleApp23
{
    class Program
    {
        static void Main(string[] args)
        {
            var keyValuePairs =
                new KeyValuePair<int, int>[3]
                {
                    new KeyValuePair<int, int>(1,1),
                    new KeyValuePair<int, int>(2,2),
                    new KeyValuePair<int, int>(3,3),
                };

                IEnumerable<KeyValuePair<int, int>> collection = keyValuePairs;

            var e = collection.GetEnumerator();

            var step2 = e.MoveNext();
            var step3 = e.Current;
            Console.WriteLine("{0} {1}", step2, step3);

            var step4 = e.MoveNext();
            var step5 = e.Current;
            Console.WriteLine("{0} {1}", step4, step5);

            var step6 = e.MoveNext();
            var step7 = e.Current;
            Console.WriteLine("{0} {1}", step6, step7);

            var step8 = e.MoveNext();
            var step9 = e.Current;
            Console.WriteLine("{0} {1}", step8, step9);
       }
    }
}
cartermp commented 5 years ago

Closing. Thanks for investigating, didn't expect it to be a .NET Core bug!

KevinRansom commented 5 years ago

So, I think I am going to provide a fix in FSharp.Core.