diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index cc503103..9eb4a986 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -5993,7 +5993,9 @@ private static bool TryEmitSwitch(SwitchExpression expr, ILGenerator il, ref Com // Check the pre-requisite conditions and emit OpCodes.Switch if possible, see #398 const int minSwitchTableSize = 3; Debug.Assert(minSwitchTableSize >= 3); // The later code will assume at least so few of the cases count - if (customEqualMethod == null && caseCount >= minSwitchTableSize && switchValueType.IsIntegerOrUnderlyingInteger()) + // The IL switch index and the normalization arithmetic must use 32-bit values. + // Wider operands retain the ordered comparison dispatch below. + if (customEqualMethod == null && caseCount >= minSwitchTableSize && switchValueType.IsIntegerOrUnderlyingIntegerUpTo32bits()) { var multipleTestValuesLabelsId = 0; var hasDuplicateTestValue = false; @@ -6198,7 +6200,7 @@ private static bool TryEmitSwitch(SwitchExpression expr, ILGenerator il, ref Com { // store and use the switch value over in case of the outlier, otherwise just use whatever is on stack from the prev TryEmit EmitStoreAndLoadLocalVariable(il, switchValVar = il.GetNextLocalVarIndex(switchValueType)); - EmitLoadConstantLong(il, switchValues.GetSurePresentRef(0).Value); // Load the min outlier + EmitLoadConstantInt(il, unchecked((int)switchValues.GetSurePresentRef(0).Value)); // Load the min outlier il.Demit(OpCodes.Beq, minOutlierLabel); EmitLoadLocalVariable(il, switchValVar); // Load the switch var on stack for the next operation } @@ -6210,8 +6212,8 @@ private static bool TryEmitSwitch(SwitchExpression expr, ILGenerator il, ref Com // Before emitting switch we need to normalize the switch value to start from zero if (firstTestValue != 0) { - EmitLoadConstantLong(il, firstTestValue); - il.Demit(firstTestValue > 0 ? OpCodes.Sub : OpCodes.Add); + EmitLoadConstantInt(il, unchecked((int)firstTestValue)); + il.Demit(OpCodes.Sub); } // Emit the switch instruction @@ -6222,7 +6224,7 @@ private static bool TryEmitSwitch(SwitchExpression expr, ILGenerator il, ref Com { Debug.Assert(switchValVar != -1); EmitLoadLocalVariable(il, switchValVar); - EmitLoadConstantLong(il, switchValues.GetSurePresentRef(switchValuesCount - 1).Value); // Load the max outlier + EmitLoadConstantInt(il, unchecked((int)switchValues.GetSurePresentRef(switchValuesCount - 1).Value)); // Load the max outlier il.Demit(OpCodes.Beq, maxOutlierLabel); // If switch value matches the outlier break to its label } @@ -9139,6 +9141,12 @@ internal static bool IsIntegerOrUnderlyingInteger(this Type type) => type.IsPrimitive && type.IsInteger() || type.IsEnum && Enum.GetUnderlyingType(type).IsInteger(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsIntegerOrUnderlyingIntegerUpTo32bits(this Type type) => + (type.IsPrimitive || type.IsEnum) && Type.GetTypeCode(type) is + TypeCode.Char or TypeCode.SByte or TypeCode.Byte or + TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32; + internal static bool IsPrimitiveWithZeroDefaultExceptDecimal(this Type type) { switch (Type.GetTypeCode(type)) diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 0e3921b6..d08ac0c9 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -46,6 +46,7 @@ static void RunLightExpressionTests(object state) t.Run(new LightExpression.UnitTests.ByRefConversionRegressionTests()); t.Run(new LightExpression.UnitTests.RuntimeTypeConstantRegressionTests()); t.Run(new LightExpression.UnitTests.ExtensionReductionIdentityRegressionTests()); + t.Run(new LightExpression.UnitTests.IntegerSwitchDispatchRegressionTests()); t.Run(new LightExpression.UnitTests.ParityTests()); Console.WriteLine($"Just LightExpression tests are passing in {justLightTestsStopwatch.ElapsedMilliseconds} ms."); @@ -71,6 +72,7 @@ static void RunLightExpressionTests(object state) fecTests.Run(new RuntimeTypeConstantRegressionTests()); fecTests.Run(new CustomReducibleNodeRegressionTests()); fecTests.Run(new ExtensionReductionIdentityRegressionTests()); + fecTests.Run(new IntegerSwitchDispatchRegressionTests()); fecTests.Run(new ParityTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index ef6fc328..b85ba34e 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -78,6 +78,7 @@ public static void Main(string[] args) lt.Run(new LightExpression.UnitTests.ByRefConversionRegressionTests()); lt.Run(new LightExpression.UnitTests.RuntimeTypeConstantRegressionTests()); lt.Run(new LightExpression.UnitTests.ExtensionReductionIdentityRegressionTests()); + lt.Run(new LightExpression.UnitTests.IntegerSwitchDispatchRegressionTests()); lt.Run(new LightExpression.UnitTests.ParityTests()); ltTime = ltStopwatch.ElapsedMilliseconds; }); @@ -90,6 +91,7 @@ public static void Main(string[] args) st.Run(new RuntimeTypeConstantRegressionTests()); st.Run(new CustomReducibleNodeRegressionTests()); st.Run(new ExtensionReductionIdentityRegressionTests()); + st.Run(new IntegerSwitchDispatchRegressionTests()); st.Run(new ParityTests()); st.Run(new Issue498_InvalidProgramException_when_using_loop()); st.Run(new Issue495_Incomplete_pattern_detection_for_NotSupported_1007_Return_goto_from_TryCatch_with_Assign_generates_invalid_IL()); diff --git a/test/FastExpressionCompiler.UnitTests/IntegerSwitchDispatchRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/IntegerSwitchDispatchRegressionTests.cs new file mode 100644 index 00000000..dcde652d --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/IntegerSwitchDispatchRegressionTests.cs @@ -0,0 +1,131 @@ +using System; +using SysExpr = System.Linq.Expressions.Expression; + +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct IntegerSwitchDispatchRegressionTests : ITestX +{ + public void Run(TestRun t) + { + Small_integer_switches_match_system(t); + Duplicate_switch_tests_choose_the_first_case(t); + Wide_switch_values_do_not_alias_low_bits(t); + Negative_and_unsigned_table_origins_match_system(t); + Wide_switches_evaluate_the_operand_once(t); + } + + public enum SignedKind : long { One = 1, Two = 2, Three = 3, Max = long.MaxValue } + public enum UnsignedKind : ulong { One = 1, Two = 2, Three = 3, Max = ulong.MaxValue } + public enum SmallKind { One = 1, Two = 2, Three = 3, Max = int.MaxValue } + + public enum SByteKind : sbyte { One = 1, Two = 2, Three = 3 } + public enum ByteKind : byte { One = 1, Two = 2, Three = 3 } + public enum Int16Kind : short { One = 1, Two = 2, Three = 3 } + public enum UInt16Kind : ushort { One = 1, Two = 2, Three = 3 } + public enum UInt32Kind : uint { One = 1, Two = 2, Three = 3 } + + public void Small_integer_switches_match_system(TestContext t) + { + foreach (var type in new[] { typeof(char), typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), + typeof(int), typeof(uint), typeof(SByteKind), typeof(ByteKind), typeof(Int16Kind), + typeof(UInt16Kind), typeof(SmallKind), typeof(UInt32Kind) }) + { + object Value(int value) => type.IsEnum ? Enum.ToObject(type, value) : Convert.ChangeType(value, type); + var parameter = SysExpr.Parameter(type); + var expression = SysExpr.Lambda(SysExpr.Switch(parameter, SysExpr.Constant(-1), + SysExpr.SwitchCase(SysExpr.Constant(10), SysExpr.Constant(Value(1), type)), + SysExpr.SwitchCase(SysExpr.Constant(20), SysExpr.Constant(Value(2), type)), + SysExpr.SwitchCase(SysExpr.Constant(30), SysExpr.Constant(Value(3), type))), parameter); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var expected = expression.Compile(); + for (var value = 0; value <= 4; ++value) + t.AreEqual(expected.DynamicInvoke(Value(value)), fast.DynamicInvoke(Value(value))); + } + } + + private static object AsValue(Type type, ulong value) => type.IsEnum + ? Enum.ToObject(type, value) + : type == typeof(long) ? (object)unchecked((long)value) + : type == typeof(ulong) ? value + : type == typeof(uint) ? (object)unchecked((uint)value) + : unchecked((int)value); + + public void Duplicate_switch_tests_choose_the_first_case(TestContext t) + { + foreach (var type in new[] { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(SignedKind), typeof(UnsignedKind), typeof(SmallKind) }) + { + var parameter = SysExpr.Parameter(type); + var maximum = type == typeof(long) || type == typeof(SignedKind) ? (ulong)long.MaxValue + : type == typeof(ulong) || type == typeof(UnsignedKind) ? ulong.MaxValue + : type == typeof(uint) ? uint.MaxValue : (ulong)int.MaxValue; + var expression = SysExpr.Lambda(SysExpr.Switch(parameter, SysExpr.Constant("default"), + SysExpr.SwitchCase(SysExpr.Constant("first"), SysExpr.Constant(AsValue(type, 1), type)), + SysExpr.SwitchCase(SysExpr.Constant("two"), SysExpr.Constant(AsValue(type, 2), type)), + SysExpr.SwitchCase(SysExpr.Constant("duplicate"), SysExpr.Constant(AsValue(type, 1), type)), + SysExpr.SwitchCase(SysExpr.Constant("max"), SysExpr.Constant(AsValue(type, maximum), type))), parameter); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var expected = expression.Compile(); + t.AreEqual("first", fast.DynamicInvoke(AsValue(type, 1))); + t.AreEqual("two", fast.DynamicInvoke(AsValue(type, 2))); + t.AreEqual("max", fast.DynamicInvoke(AsValue(type, maximum))); + foreach (var value in new[] { 0UL, 1UL, 2UL, 3UL, maximum }) + t.AreEqual(expected.DynamicInvoke(AsValue(type, value)), fast.DynamicInvoke(AsValue(type, value))); + } + } + + public void Wide_switch_values_do_not_alias_low_bits(TestContext t) + { + foreach (var type in new[] { typeof(long), typeof(ulong), typeof(SignedKind), typeof(UnsignedKind) }) + { + var parameter = SysExpr.Parameter(type); + var expression = SysExpr.Lambda(SysExpr.Switch(parameter, SysExpr.Constant(-1), + SysExpr.SwitchCase(SysExpr.Constant(10), SysExpr.Constant(AsValue(type, 1), type)), + SysExpr.SwitchCase(SysExpr.Constant(20), SysExpr.Constant(AsValue(type, 2), type)), + SysExpr.SwitchCase(SysExpr.Constant(30), SysExpr.Constant(AsValue(type, 3), type))), parameter); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var expected = expression.Compile(); + foreach (var value in new[] { 0UL, 1UL, 2UL, 3UL, 4UL, (1UL << 32) + 1, (1UL << 32) + 2, ulong.MaxValue, (ulong)long.MaxValue }) + t.AreEqual(expected.DynamicInvoke(AsValue(type, value)), fast.DynamicInvoke(AsValue(type, value))); + } + } + + public void Negative_and_unsigned_table_origins_match_system(TestContext t) + { + foreach (var type in new[] { typeof(int), typeof(uint) }) + foreach (var origin in type == typeof(int) ? new[] { int.MinValue, -3, -2, 0, 1, int.MaxValue - 3 } : new[] { 0, 1, int.MinValue, -4 }) + { + var parameter = SysExpr.Parameter(type); + object Value(int offset) => type == typeof(int) ? (object)unchecked(origin + offset) : unchecked((uint)(origin + offset)); + var expression = SysExpr.Lambda(SysExpr.Switch(parameter, SysExpr.Constant(-1), + SysExpr.SwitchCase(SysExpr.Constant(10), SysExpr.Constant(Value(0), type)), + SysExpr.SwitchCase(SysExpr.Constant(20), SysExpr.Constant(Value(1), type)), + SysExpr.SwitchCase(SysExpr.Constant(30), SysExpr.Constant(Value(2), type))), parameter); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var expected = expression.Compile(); + foreach (var offset in new[] { -1, 0, 1, 2, 3, 5 }) + t.AreEqual(expected.DynamicInvoke(Value(offset)), fast.DynamicInvoke(Value(offset))); + } + } + + public void Wide_switches_evaluate_the_operand_once(TestContext t) + { + var parameter = SysExpr.Parameter(typeof(Func)); + var expression = SysExpr.Lambda, int>>(SysExpr.Switch(SysExpr.Invoke(parameter), SysExpr.Constant(-1), + SysExpr.SwitchCase(SysExpr.Constant(10), SysExpr.Constant(1L)), + SysExpr.SwitchCase(SysExpr.Constant(20), SysExpr.Constant(2L)), + SysExpr.SwitchCase(SysExpr.Constant(30), SysExpr.Constant(3L))), parameter); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var calls = 0; + t.AreEqual(20, fast(() => { ++calls; return 2; })); + t.AreEqual(1, calls); + } +}