A colleague has passed me an interesting code sample that crashes with an InvalidProgramException
("CLR detected an Invalid Program") when run.
The problem seems to occur at JIT time, in that this compiles fine but throws the exception just before the method with the "offending" line is called - I guess as it is being JIT'd.
The line in question is calling Enumerable.ToDictionary
and passing in a Func
as the second argument.
If the Func
argument is fully specified with a lambda it works; if it is specified as a method group, if fails. Surely these two are equivalent?
This has me stumped (and the colleague who discovered it!) - and it certainly seems like a JIT error.
[EDIT: Sorry - I got the pass and fail cases the wrong way round in the code sample - now corrected (description above was correct)]
Does anyone have an explanation?
using System;
using System.Linq;
internal class Program
{
private static void Main(string[] args)
{
Test.Try();
}
}
public class Test
{
public static readonly int[] integers = new[] { 1, 3, 5 };
public static void Try()
{
var line = new Line { A = 3, B = 5 };
// PASSES
var dict = integers.ToDictionary<int, int, decimal>(i => i, i => line.Compute(i));
// FAILS
//var dict = integers.ToDictionary<int, int, decimal>(i => i, line.Compute);
Console.WriteLine(string.Join(" ", dict.Select(kv => kv.Key + "-" + kv.Value)));
}
}
public class Line
{
public decimal A;
public decimal B;
}
public static class SimpleCompute
{
public static decimal Compute(this Line line, int value)
{
return line.A*value + line.B;
}
}
Compiler bug.
For info, I have the async CTP, which might be related; csc
reports: 4.0.30319.440
Seems to be a difference between:
public static void TryTwo() // fails
{
var line = new Line {A = 3, B = 5};
var dict = integers.ToDictionary<int, int, decimal>(i => i, line.Compute);
Console.WriteLine("TryTwo complete");
}
public static void TryFive() // works
{
var line = new Line { A = 3, B = 5 };
Func<int, decimal> func = line.Compute;
var dict = integers.ToDictionary<int, int, decimal>(i => i, func);
Console.WriteLine("TryFour complete");
}
so let's look in reflector:
.method public hidebysig static void TryTwo() cil managed
{
.maxstack 4
.locals init (
[0] class Line <>g__initLocal6)
L_0000: newobj instance void Line::.ctor()
L_0005: stloc.0
L_0006: ldloc.0
L_0007: ldc.i4.3
L_0008: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
L_000d: stfld valuetype [mscorlib]System.Decimal Line::A
L_0012: ldloc.0
L_0013: ldc.i4.5
L_0014: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
L_0019: stfld valuetype [mscorlib]System.Decimal Line::B
L_001e: ldsfld int32[] Test::integers
L_0023: ldsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegate8
L_0028: brtrue.s L_003b
L_002a: ldnull
L_002b: ldftn int32 Test::<TryTwo>b__7(int32)
L_0031: newobj instance void [mscorlib]System.Func`2<int32, int32>::.ctor(object, native int)
L_0036: stsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegate8
L_003b: ldsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegate8
L_0040: call class [mscorlib]System.Collections.Generic.Dictionary`2<!!1, !!2> [System.Core]System.Linq.Enumerable::ToDictionary<int32, int32, valuetype [mscorlib]System.Decimal>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, !!1>, class [mscorlib]System.Func`2<!!0, !!2>)
L_0045: pop
L_0046: ldstr "TryTwo complete"
L_004b: call void [mscorlib]System.Console::WriteLine(string)
L_0050: ret
}
vs
.method public hidebysig static void TryFive() cil managed
{
.maxstack 4
.locals init (
[0] class Line line,
[1] class [mscorlib]System.Func`2<int32, valuetype [mscorlib]System.Decimal> func,
[2] class Line <>g__initLocal9)
L_0000: newobj instance void Line::.ctor()
L_0005: stloc.2
L_0006: ldloc.2
L_0007: ldc.i4.3
L_0008: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
L_000d: stfld valuetype [mscorlib]System.Decimal Line::A
L_0012: ldloc.2
L_0013: ldc.i4.5
L_0014: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
L_0019: stfld valuetype [mscorlib]System.Decimal Line::B
L_001e: ldloc.2
L_001f: stloc.0
L_0020: ldloc.0
L_0021: ldftn valuetype [mscorlib]System.Decimal SimpleCompute::Compute(class Line, int32)
L_0027: newobj instance void [mscorlib]System.Func`2<int32, valuetype [mscorlib]System.Decimal>::.ctor(object, native int)
L_002c: stloc.1
L_002d: ldsfld int32[] Test::integers
L_0032: ldsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegateb
L_0037: brtrue.s L_004a
L_0039: ldnull
L_003a: ldftn int32 Test::<TryFive>b__a(int32)
L_0040: newobj instance void [mscorlib]System.Func`2<int32, int32>::.ctor(object, native int)
L_0045: stsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegateb
L_004a: ldsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegateb
L_004f: ldloc.1
L_0050: call class [mscorlib]System.Collections.Generic.Dictionary`2<!!1, !!2> [System.Core]System.Linq.Enumerable::ToDictionary<int32, int32, valuetype [mscorlib]System.Decimal>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, !!1>, class [mscorlib]System.Func`2<!!0, !!2>)
L_0055: pop
L_0056: ldstr "TryFour complete"
L_005b: call void [mscorlib]System.Console::WriteLine(string)
L_0060: ret
}
If you look in the broken version, it only loads one delegate. Compiler bug, basically:
L_0023: ldsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegate8
L_0028: brtrue.s L_003b
L_002a: ldnull
L_002b: ldftn int32 Test::<TryTwo>b__7(int32)
L_0031: newobj instance void [mscorlib]System.Func`2<int32, int32>::.ctor(object, native int)
L_0036: stsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegate8
L_003b: ldsfld class [mscorlib]System.Func`2<int32, int32> Test::CS$<>9__CachedAnonymousMethodDelegate8
L_0040: call class [mscorlib]System.Collections.Generic.Dictionary`2<!!1, !!2> [System.Core]System.Linq.Enumerable::ToDictionary<int32, int32, valuetype [mscorlib]System.Decimal>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, !!1>, class [mscorlib]System.Func`2<!!0, !!2>)
all of the above is "check whether the cached i => i
exists; if not create it; then load it". It never does anything with the second delegate. Consequently, there aren't enough values on the stack to make the method call.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With