From b3035687ff7d3e01a8084c40be94d1004c924630 Mon Sep 17 00:00:00 2001 From: SychicBoy Date: Sun, 16 Aug 2026 22:06:17 +0330 Subject: [PATCH 1/4] Recognize negated floating-point special constants --- .../TestCases/Pretty/WellKnownConstants.cs | 2 ++ .../CSharp/Syntax/TypeSystemAstBuilder.cs | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs index c26aeb947a..5a17334213 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs @@ -61,6 +61,7 @@ public class WellKnownConstants public const float FloatMaxValue = float.MaxValue; public const float FloatMinValue = float.MinValue; public const float FloatEpsilon = float.Epsilon; + public const float FloatNegativeEpsilon = -float.Epsilon; public const double DoubleZero = 0.0; public const double DoubleMinusZero = -0.0; @@ -70,6 +71,7 @@ public class WellKnownConstants public const double DoubleMaxValue = double.MaxValue; public const double DoubleMinValue = double.MinValue; public const double DoubleEpsilon = double.Epsilon; + public const double DoubleNegativeEpsilon = -double.Epsilon; public const decimal DecimalMaxValue = decimal.MaxValue; public const decimal DecimalMinValue = decimal.MinValue; diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index 70c880fd02..231e0f41ae 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -1167,8 +1167,22 @@ public Expression ConvertConstantValue(IType expectedType, IType type, object? c bool IsSpecialConstant(IType expectedType, object constant, [NotNullWhen(true)] out Expression? expression) { expression = null; + bool negate = false; if (!specialConstants.TryGetValue(constant, out var info)) - return false; + { + if (constant is float single && single < 0f && specialConstants.TryGetValue(-single, out info)) + { + negate = true; + } + else if (constant is double dbl && dbl < 0.0 && specialConstants.TryGetValue(-dbl, out info)) + { + negate = true; + } + else + { + return false; + } + } // find IType of constant in compilation. var constantType = expectedType; if (!expectedType.IsKnownType(info.Type)) @@ -1245,6 +1259,13 @@ bool IsSpecialConstant(IType expectedType, object constant, [NotNullWhen(true)] if (AddResolveResultAnnotations) expression.AddAnnotation(new MemberResolveResult(new TypeResolveResult(constantType), field)); + if (negate) + { + expression = new UnaryOperatorExpression(UnaryOperatorType.Minus, expression); + if (AddResolveResultAnnotations) + expression.AddAnnotation(new ConstantResolveResult(constantType, constant)); + } + return true; } From 59361d9de2c4cc52236fbecea98878e211aee5c1 Mon Sep 17 00:00:00 2001 From: SychicBoy Date: Sun, 16 Aug 2026 22:07:22 +0330 Subject: [PATCH 2/4] Prefer common machine-scale floating-point fractions --- .../TestCases/Pretty/WellKnownConstants.cs | 11 ++ .../CSharp/Syntax/TypeSystemAstBuilder.cs | 129 +++++++++++++++++- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs index 5a17334213..50c3687143 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs @@ -84,6 +84,17 @@ public class WellKnownConstants public const double Double_Sixth = 1.0 / 6.0; public const float Float_Tenth = 0.1f; public const double Double_Tenth = 0.1; + public const float Float_Third = 1f / 3f; + public const float Float_PowerOfTwoFraction = 21f / 32f; + public const float Float_SmoothFraction = 2f / 15f; + public const float Float_UnitFraction = 1f / 85f; + public const float Float_ByteScale_225 = 225f / 255f; + public const float Float_ByteScale_200 = 200f / 255f; + public const float Float_ByteScale_150 = 150f / 255f; + public const float Float_NegativeByteScale = -200f / 255f; + public const double Double_ByteScale = 200.0 / 255.0; + public const float Float_KScale = 123f / 1024f; + public const float Float_MScale = 123f / 1048576f; #if ROSLYN2 && !NET40 public const float Float_PI = MathF.PI; diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index 231e0f41ae..0a3e9eb141 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -1516,6 +1516,103 @@ static bool IsEqual(long num, long den, object constantValue, bool isDouble) const int MAX_DENOMINATOR_DOUBLE = 1000; const int MAX_DENOMINATOR_FLOAT = 360; + // Common machine-scale denominators: powers of two used for binary scaling, + // and 2^n-1 values used when normalizing integers (for example, byte colors / 255). + // Keep this as a targeted candidate set rather than increasing the generic denominator + // limit, which would reintroduce accidental fraction matches for ordinary floating-point values. + static readonly int[] preferredFractionDenominators = { + 127, 128, + 255, 256, + 1023, 1024, + 4095, 4096, + 8192, + 16384, + 32767, 32768, + 65535, 65536, + 1048576 + }; + + static int GetIntegerLiteralLength(long value) + { + int length = value < 0 ? 1 : 0; + do + { + length++; + value /= 10; + } while (value != 0); + return length; + } + + static int GetFractionDisplayLength(long num, long den, bool isDouble) + { + // Float integer constants use the `f` suffix; double constants need `.0`. + int numericSuffixLength = isDouble ? 2 : 1; + return GetIntegerLiteralLength(num) + GetIntegerLiteralLength(den) + + 3 + 2 * numericSuffixLength; // `num / den` + } + + // Unit fractions and denominators composed only of 2, 3 and 5 are already + // conventional forms; do not expand them just to reach a preferred scale. + static bool IsSimpleFraction(long num, long den) + { + Debug.Assert(den > 0); + if (num == 1 || num == -1) + return true; + while (den % 2 == 0) + den /= 2; + while (den % 3 == 0) + den /= 3; + while (den % 5 == 0) + den /= 5; + return den == 1; + } + + static int GetPreferredFractionScore(long num, int den, bool isDouble) + { + // Powers of two are native to binary floating point and therefore more likely to + // match by coincidence. Values of the form 2^n-1 are a stronger normalization + // signal, so allow them a slightly larger readability bonus. + bool isPowerOfTwo = (den & (den - 1)) == 0; + Debug.Assert(isPowerOfTwo || ((den + 1) & den) == 0); + int readabilityBonus = isPowerOfTwo ? 1 : 2; + return GetFractionDisplayLength(num, den, isDouble) - readabilityBonus; + } + + static bool TryGetPreferredFraction(object constantValue, bool isDouble, out long num, out long den, out int score) + { + num = 0; + den = 0; + score = int.MaxValue; + + double value = isDouble ? (double)constantValue : (float)constantValue; + if (!(Math.Abs(value) < 1.0)) + return false; + + foreach (int candidateDen in preferredFractionDenominators) + { + long candidateNum = (long)Math.Round(value * candidateDen); + if (candidateNum == 0 || candidateNum <= -candidateDen || candidateNum >= candidateDen) + continue; + if (!IsEqual(candidateNum, candidateDen, constantValue, isDouble)) + continue; + + int candidateScore = GetPreferredFractionScore(candidateNum, candidateDen, isDouble); + if (candidateScore < score || (candidateScore == score && candidateDen < den)) + { + num = candidateNum; + den = candidateDen; + score = candidateScore; + } + } + + return den != 0; + } + + Expression MakeFraction(IType type, long num, long den) + { + return new BinaryOperatorExpression(MakeConstant(type, num), BinaryOperatorType.Divide, MakeConstant(type, den)); + } + Expression ConvertFloatingPointLiteral(IType type, object constantValue) { // Coerce constantValue to either float or double: @@ -1557,11 +1654,35 @@ Expression ConvertFloatingPointLiteral(IType type, object constantValue) ? FractionApprox((double)constantValue, MAX_DENOMINATOR_DOUBLE) : FractionApprox((float)constantValue, 200); - if (IsValidFraction(num, den) && IsEqual(num, den, constantValue, isDouble) && Math.Abs(den) != 1) + bool hasRegularFraction = IsValidFraction(num, den) + && IsEqual(num, den, constantValue, isDouble) + && Math.Abs(den) != 1; + + if (TryGetPreferredFraction(constantValue, isDouble, out long preferredNum, out long preferredDen, out int preferredScore)) + { + int baselineLength = hasRegularFraction + ? GetFractionDisplayLength(num, den, isDouble) + : str.Length + (isDouble ? 0 : 1); + // Do not replace an already-simple fraction (for example 21 / 32 or 2 / 15) + // just to reach one of the larger preferred scales. + bool regularFractionIsSimple = hasRegularFraction && IsSimpleFraction(num, den); + if (!regularFractionIsSimple && preferredScore <= baselineLength) + { + if (hasRegularFraction) + { + num = preferredNum; + den = preferredDen; + } + else + { + expr = MakeFraction(type, preferredNum, preferredDen); + } + } + } + + if (hasRegularFraction) { - var left = MakeConstant(type, num); - var right = MakeConstant(type, den); - expr = new BinaryOperatorExpression(left, BinaryOperatorType.Divide, right); + expr = MakeFraction(type, num, den); } } From b209b3972975aa2945c6ea9443542d56e9522319 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 17 Aug 2026 20:18:20 +0200 Subject: [PATCH 3/4] Drive negated special constants from the lookup table The sign of a constant cannot decide whether to emit a unary minus: MinValue and NegativeInfinity are negative, yet are their own members and must not be negated. Deriving it that way emits -float.MinValue for float.MinValue, which is a different value. Which constants are reachable by negation is also not obvious: -MaxValue is exactly MinValue and both infinities have their own members, so Epsilon is the only one, but establishing that takes a proof rather than a read. Recording it in the table states the invariant instead, and leaves the lookup itself as the single dictionary probe it was before. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../CSharp/Syntax/TypeSystemAstBuilder.cs | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index 0a3e9eb141..e3c66d95e8 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -1167,22 +1167,8 @@ public Expression ConvertConstantValue(IType expectedType, IType type, object? c bool IsSpecialConstant(IType expectedType, object constant, [NotNullWhen(true)] out Expression? expression) { expression = null; - bool negate = false; if (!specialConstants.TryGetValue(constant, out var info)) - { - if (constant is float single && single < 0f && specialConstants.TryGetValue(-single, out info)) - { - negate = true; - } - else if (constant is double dbl && dbl < 0.0 && specialConstants.TryGetValue(-dbl, out info)) - { - negate = true; - } - else - { - return false; - } - } + return false; // find IType of constant in compilation. var constantType = expectedType; if (!expectedType.IsKnownType(info.Type)) @@ -1259,7 +1245,7 @@ bool IsSpecialConstant(IType expectedType, object constant, [NotNullWhen(true)] if (AddResolveResultAnnotations) expression.AddAnnotation(new MemberResolveResult(new TypeResolveResult(constantType), field)); - if (negate) + if (info.Negate) { expression = new UnaryOperatorExpression(UnaryOperatorType.Minus, expression); if (AddResolveResultAnnotations) @@ -1269,44 +1255,50 @@ bool IsSpecialConstant(IType expectedType, object constant, [NotNullWhen(true)] return true; } - static readonly Dictionary specialConstants = new Dictionary() { + // Negate wraps the member reference in a unary minus. The negative counterpart of most + // members is itself a member (-MaxValue is exactly MinValue, and both infinities have + // their own), so only Epsilon needs it. It cannot be derived from the sign of the + // constant: MinValue and NegativeInfinity are negative, but must not be negated. + static readonly Dictionary specialConstants = new Dictionary() { // byte: - { byte.MaxValue, (KnownTypeCode.Byte, "MaxValue") }, + { byte.MaxValue, (KnownTypeCode.Byte, "MaxValue", false) }, // sbyte: - { sbyte.MinValue, (KnownTypeCode.SByte, "MinValue") }, - { sbyte.MaxValue, (KnownTypeCode.SByte, "MaxValue") }, + { sbyte.MinValue, (KnownTypeCode.SByte, "MinValue", false) }, + { sbyte.MaxValue, (KnownTypeCode.SByte, "MaxValue", false) }, // short: - { short.MinValue, (KnownTypeCode.Int16, "MinValue") }, - { short.MaxValue, (KnownTypeCode.Int16, "MaxValue") }, + { short.MinValue, (KnownTypeCode.Int16, "MinValue", false) }, + { short.MaxValue, (KnownTypeCode.Int16, "MaxValue", false) }, // ushort: - { ushort.MaxValue, (KnownTypeCode.UInt16, "MaxValue") }, + { ushort.MaxValue, (KnownTypeCode.UInt16, "MaxValue", false) }, // int: - { int.MinValue, (KnownTypeCode.Int32, "MinValue") }, - { int.MaxValue, (KnownTypeCode.Int32, "MaxValue") }, + { int.MinValue, (KnownTypeCode.Int32, "MinValue", false) }, + { int.MaxValue, (KnownTypeCode.Int32, "MaxValue", false) }, // uint: - { uint.MaxValue, (KnownTypeCode.UInt32, "MaxValue") }, + { uint.MaxValue, (KnownTypeCode.UInt32, "MaxValue", false) }, // long: - { long.MinValue, (KnownTypeCode.Int64, "MinValue") }, - { long.MaxValue, (KnownTypeCode.Int64, "MaxValue") }, + { long.MinValue, (KnownTypeCode.Int64, "MinValue", false) }, + { long.MaxValue, (KnownTypeCode.Int64, "MaxValue", false) }, // ulong: - { ulong.MaxValue, (KnownTypeCode.UInt64, "MaxValue") }, + { ulong.MaxValue, (KnownTypeCode.UInt64, "MaxValue", false) }, // float: - { float.NaN, (KnownTypeCode.Single, "NaN") }, - { float.NegativeInfinity, (KnownTypeCode.Single, "NegativeInfinity") }, - { float.PositiveInfinity, (KnownTypeCode.Single, "PositiveInfinity") }, - { float.MinValue, (KnownTypeCode.Single, "MinValue") }, - { float.MaxValue, (KnownTypeCode.Single, "MaxValue") }, - { float.Epsilon, (KnownTypeCode.Single, "Epsilon") }, + { float.NaN, (KnownTypeCode.Single, "NaN", false) }, + { float.NegativeInfinity, (KnownTypeCode.Single, "NegativeInfinity", false) }, + { float.PositiveInfinity, (KnownTypeCode.Single, "PositiveInfinity", false) }, + { float.MinValue, (KnownTypeCode.Single, "MinValue", false) }, + { float.MaxValue, (KnownTypeCode.Single, "MaxValue", false) }, + { float.Epsilon, (KnownTypeCode.Single, "Epsilon", false) }, + { -float.Epsilon, (KnownTypeCode.Single, "Epsilon", true) }, // double: - { double.NaN, (KnownTypeCode.Double, "NaN") }, - { double.NegativeInfinity, (KnownTypeCode.Double, "NegativeInfinity") }, - { double.PositiveInfinity, (KnownTypeCode.Double, "PositiveInfinity") }, - { double.MinValue, (KnownTypeCode.Double, "MinValue") }, - { double.MaxValue, (KnownTypeCode.Double, "MaxValue") }, - { double.Epsilon, (KnownTypeCode.Double, "Epsilon") }, + { double.NaN, (KnownTypeCode.Double, "NaN", false) }, + { double.NegativeInfinity, (KnownTypeCode.Double, "NegativeInfinity", false) }, + { double.PositiveInfinity, (KnownTypeCode.Double, "PositiveInfinity", false) }, + { double.MinValue, (KnownTypeCode.Double, "MinValue", false) }, + { double.MaxValue, (KnownTypeCode.Double, "MaxValue", false) }, + { double.Epsilon, (KnownTypeCode.Double, "Epsilon", false) }, + { -double.Epsilon, (KnownTypeCode.Double, "Epsilon", true) }, // decimal: - { decimal.MinValue, (KnownTypeCode.Decimal, "MinValue") }, - { decimal.MaxValue, (KnownTypeCode.Decimal, "MaxValue") }, + { decimal.MinValue, (KnownTypeCode.Decimal, "MinValue", false) }, + { decimal.MaxValue, (KnownTypeCode.Decimal, "MaxValue", false) }, }; bool IsFlagsEnum(ITypeDefinition type) From 2311b64409ac8141b225d0afb36b5466bb92b999 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 18 Aug 2026 05:27:46 +0200 Subject: [PATCH 4/4] Cover the fractions that were not written as fractions The preferred-scale lookup reaches well past the byte-normalization cases it was added for: any value that is exactly n/2^k for a k the old denominator limit could not reach now prints as a fraction, so constants that used to be short exact decimals changed shape. That is the widest-reaching part of the change and nothing pinned it. The added constants are those values, including the unreduced 126 / 1024 that a lowest-terms rewrite would turn into 63 / 512, plus two that must keep their decimal form so the length gate stays covered from both sides. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../TestCases/Pretty/WellKnownConstants.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs index 50c3687143..8f1506401b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/WellKnownConstants.cs @@ -95,6 +95,14 @@ public class WellKnownConstants public const double Double_ByteScale = 200.0 / 255.0; public const float Float_KScale = 123f / 1024f; public const float Float_MScale = 123f / 1048576f; + public const float Float_UnitKScale = 1f / 1024f; + public const float Float_SmallKScale = 17f / 1024f; + public const float Float_UnreducedKScale = 126f / 1024f; + public const float Float_ShortBinaryFraction = 13f / 256f; + public const float Float_NearOnePowerOfTwo = 225f / 256f; + public const float Float_TinyPowerOfTwoScale = 1f / 32768f; + public const float Float_NotAPreferredScale = 0.123456f; + public const double Double_NotAPreferredScale = 0.123456789; #if ROSLYN2 && !NET40 public const float Float_PI = MathF.PI;