diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index 4ad298c7c..799f91517 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -83,8 +83,14 @@ namespace ICSharpCode.Decompiler.Tests } [Test] - public void FloatingPointArithmetic([ValueSource("defaultOptions")] CompilerOptions options) - { + public void FloatingPointArithmetic([ValueSource("noMonoOptions")] CompilerOptions options, [Values(32, 64)] int bits) + { + // The behavior of the #1794 incorrect `(float)(double)val` cast only causes test failures + // for some runtime+compiler combinations. + if (bits == 32) + options |= CompilerOptions.Force32Bit; + // Mono is excluded because we never use it for the second pass, so the test ends up failing + // due to some Mono vs. Roslyn compiler differences. RunCS(options: options); } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/FloatingPointArithmetic.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/FloatingPointArithmetic.cs index 72c0bbc72..e5a8fa9f3 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/FloatingPointArithmetic.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/FloatingPointArithmetic.cs @@ -8,12 +8,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness { Issue999(); Issue1656(); + Issue1794(); return 0; } static void Issue999() { - for (float i = -10f; i <= 10f; i += 0.01f) + for (float i = -10f; i <= 10f; i += 0.01f) Console.WriteLine("{1:R}: {0:R}", M(i), i); } @@ -31,6 +32,63 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness CxAssert((primary--) == 'C'); } + static void Issue1794() + { + Console.WriteLine("CastUnsignedToFloat:"); + Console.WriteLine(CastUnsignedToFloat(9007199791611905).ToString("r")); + Console.WriteLine("CastUnsignedToDouble:"); + Console.WriteLine(CastUnsignedToDouble(9007199791611905).ToString("r")); + Console.WriteLine("CastUnsignedToFloatViaDouble:"); + Console.WriteLine(CastUnsignedToFloatViaDouble(9007199791611905).ToString("r")); + + Console.WriteLine("CastSignedToFloat:"); + Console.WriteLine(CastSignedToFloat(9007199791611905).ToString("r")); + Console.WriteLine("ImplicitCastSignedToFloat:"); + Console.WriteLine(ImplicitCastSignedToFloat(9007199791611905).ToString("r")); + Console.WriteLine("CastSignedToDouble:"); + Console.WriteLine(CastSignedToDouble(9007199791611905).ToString("r")); + Console.WriteLine("CastSignedToFloatViaDouble:"); + Console.WriteLine(CastSignedToFloatViaDouble(9007199791611905).ToString("r")); + } + + static float CastUnsignedToFloat(ulong val) + { + return (float)val; + } + + static double CastUnsignedToDouble(ulong val) + { + return (double)val; + } + + static float CastUnsignedToFloatViaDouble(ulong val) + { + // The double-rounding can increase the rounding error + return (float)(double)val; + } + + static float CastSignedToFloat(long val) + { + return (float)val; + } + + + static double CastSignedToDouble(long val) + { + return (double)val; + } + + static float CastSignedToFloatViaDouble(long val) + { + // The double-rounding can increase the rounding error + return (float)(double)val; + } + + static float ImplicitCastSignedToFloat(long val) + { + return val; + } + static void CxAssert(bool v) { if (!v) { diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 8cbb8a1ff..2bea3eb93 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -666,7 +666,7 @@ namespace ICSharpCode.Decompiler.IL case ILOpCode.Conv_u: return Push(new Conv(Pop(), PrimitiveType.U, false, Sign.None)); case ILOpCode.Conv_r_un: - return Push(new Conv(Pop(), PrimitiveType.R8, false, Sign.Unsigned)); + return Push(new Conv(Pop(), PrimitiveType.R, false, Sign.Unsigned)); case ILOpCode.Conv_ovf_i1: return Push(new Conv(Pop(), PrimitiveType.I1, true, Sign.Signed)); case ILOpCode.Conv_ovf_i2: diff --git a/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs b/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs index 1303102e4..32c0230a3 100644 --- a/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs +++ b/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs @@ -38,17 +38,14 @@ namespace ICSharpCode.Decompiler.IL return StackType.I8; case PrimitiveType.I: case PrimitiveType.U: - case (PrimitiveType)0x0f: // Ptr - case (PrimitiveType)0x1b: // FnPtr return StackType.I; case PrimitiveType.R4: return StackType.F4; case PrimitiveType.R8: + case PrimitiveType.R: return StackType.F8; - case (PrimitiveType)0x10: // ByRef + case PrimitiveType.Ref: // ByRef return StackType.Ref; - case (PrimitiveType)0x01: // Void - return StackType.Void; case PrimitiveType.Unknown: return StackType.Unknown; default: @@ -65,6 +62,7 @@ namespace ICSharpCode.Decompiler.IL case PrimitiveType.I8: case PrimitiveType.R4: case PrimitiveType.R8: + case PrimitiveType.R: case PrimitiveType.I: return Sign.Signed; case PrimitiveType.U1: @@ -100,6 +98,7 @@ namespace ICSharpCode.Decompiler.IL case PrimitiveType.I8: case PrimitiveType.R8: case PrimitiveType.U8: + case PrimitiveType.R: return 8; case PrimitiveType.I: case PrimitiveType.U: @@ -126,6 +125,18 @@ namespace ICSharpCode.Decompiler.IL return primitiveType.GetStackType().IsIntegerType(); } + public static bool IsFloatType(this PrimitiveType type) + { + switch (type) { + case PrimitiveType.R4: + case PrimitiveType.R8: + case PrimitiveType.R: + return true; + default: + return false; + } + } + /// /// Infers the C# type for an IL instruction. /// diff --git a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs index ad88befba..6b23db08d 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs @@ -48,7 +48,7 @@ namespace ICSharpCode.Decompiler.IL /// /// Converts from the current precision available on the evaluation stack to the precision specified by /// the TargetType. - /// Uses "round-to-nearest" mode is the precision is reduced. + /// Uses "round-to-nearest" mode if the precision is reduced. /// FloatPrecisionChange, /// @@ -156,7 +156,7 @@ namespace ICSharpCode.Decompiler.IL public Conv(ILInstruction argument, StackType inputType, Sign inputSign, PrimitiveType targetType, bool checkForOverflow, bool isLifted = false) : base(OpCode.Conv, argument) { - bool needsSign = checkForOverflow || (!inputType.IsFloatType() && (targetType == PrimitiveType.R4 || targetType == PrimitiveType.R8)); + bool needsSign = checkForOverflow || (!inputType.IsFloatType() && targetType.IsFloatType()); Debug.Assert(!(needsSign && inputSign == Sign.None)); this.InputSign = needsSign ? inputSign : Sign.None; this.InputType = inputType; @@ -264,6 +264,7 @@ namespace ICSharpCode.Decompiler.IL default: return ConversionKind.Invalid; } + case PrimitiveType.R: case PrimitiveType.R8: switch (inputType) { case StackType.I4: diff --git a/ICSharpCode.Decompiler/IL/PrimitiveType.cs b/ICSharpCode.Decompiler/IL/PrimitiveType.cs index d93d60a8e..9d45d541d 100644 --- a/ICSharpCode.Decompiler/IL/PrimitiveType.cs +++ b/ICSharpCode.Decompiler/IL/PrimitiveType.cs @@ -35,7 +35,18 @@ namespace ICSharpCode.Decompiler.IL U8 = PrimitiveTypeCode.UInt64, I = PrimitiveTypeCode.IntPtr, U = PrimitiveTypeCode.UIntPtr, + /// Managed reference Ref = 16, + /// Floating point type of unspecified size: + /// usually 80 bits on x86 (when the runtime uses x87 instructions); + /// but only 64-bit on x64. + /// This only occurs for "conv.r.un" instructions. The C# compiler usually follows those + /// with a "conv.r4" or "conv.r8" instruction to indicate the desired float type, so + /// we only use this as conversion target type and don't bother tracking it as its own stack type: + /// basically everything treats R identical to R8, except for the (conv.r.un + conv.r[48] => conv.r[48].un) + /// combining logic which should not combine (conv.r.un + conv.r8 + conv.r4) into a single conv.r4.un. + /// + R = 254, Unknown = 255 } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index b2b9b4c13..3a4dd4933 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -177,6 +177,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms context.Step("conv.i4(ldlen array) => ldlen.i4(array)", inst); inst.AddILRange(inst.Argument); inst.ReplaceWith(new LdLen(inst.TargetType.GetStackType(), array).WithILRange(inst)); + return; + } + if (inst.TargetType.IsFloatType() && inst.Argument is Conv conv + && conv.Kind == ConversionKind.IntToFloat && conv.TargetType == PrimitiveType.R) + { + // IL conv.r.un does not indicate whether to convert the target type to R4 or R8, + // so the C# compiler usually follows it with an explicit conv.r4 or conv.r8. + // To avoid emitting '(float)(double)val', we combine these two conversions: + context.Step("conv.rN(conv.r.un(...)) => conv.rN.un(...)", inst); + inst.ReplaceWith(new Conv(conv.Argument, conv.InputType, conv.InputSign, inst.TargetType, inst.CheckForOverflow, inst.IsLifted | conv.IsLifted)); + return; } } diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeUtils.cs b/ICSharpCode.Decompiler/TypeSystem/TypeUtils.cs index 8663a7926..4741fef7b 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeUtils.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeUtils.cs @@ -398,6 +398,7 @@ namespace ICSharpCode.Decompiler.TypeSystem case PrimitiveType.R4: return KnownTypeCode.Single; case PrimitiveType.R8: + case PrimitiveType.R: return KnownTypeCode.Double; case PrimitiveType.U1: return KnownTypeCode.Byte;